sábado, 10 de dezembro de 2016

Usando SQLite e padrão DAO para persistir dados

Uma das formas de persistir os dados em um aplicativo nativo Android é por meio do SQLite. Ele é um mini-SGBD que vai acoplado junto a sua aplicação. Para usá-lo, você pode criar uma classe padrão DAO (Data Access Object), utilizada para manipulação de dados, que irá gerenciar os dados conjuntamente com o SQLite. Para exemplificar, vamos utilizar um model Usuario que possui um id do tipo long, um nome do tipo String e um e-mail do tipo String como variáveis de instância. Então, vamos criar um UsuarioDAO, que irá criar o banco de dados, uma tabela de usuários e cuidará da manipulação e recuperação de usuarios do banco da dados.

Para isso, crie uma classe UsuarioDAO e faça com que ela herde de SQLiteOpenHelper. Depois sobrescreva os métodos onCreate e onUpgrade. O método onCreate é executado sempre que a aplicação é instalada e utilizada pela primeira vez. O método onUpgrade é executado sempre que ocorre uma mudança de versão do banco de dados. Tendo em vista essa características, vamos criar a tabela de Usuarios no onCreate. O código abaixo servirá para exemplificar o que foi dito até aqui:

public class UsuarioDAO extends SQLiteOpenHelper {
    private static final String DATABASE = "appExemplo";
    private static final String TABELA = "Usuarios";
    private static final int VERSAO = 1;

    public UsuarioDAO(Context context) {
        super(context, DATABASE, null, VERSAO);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "CREATE TABLE " + TABLE + " ("
            + COL_ID + " INTEGER PRIMARY KEY, "
            + COL_NOME + " TEXT UNIQUE NOT NULL, "
            + COL_EMAIL + " TEXT"
            + ");";
       
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql = "DROP TABLE IF EXISTS " + TABLE;

        db.execSQL(sql);
        onCreate(db);
    }
}


É preciso tomar muito cuidado com o uso da versão. Ela deve começar com o valor 1 e sempre que ocorrer qualquer mudança na estrutura do banco de dados, ela deve ser alterada sempre acrescentando um valor a mais. Ou seja, inicia-se com 1, caso seja acrescentado um atributo novo como telefone, ou um atributo do banco tenha tido seu nome alterado, então, a próxima versão deve ter o valor 2 e depois 3, 4 e assim por diante. Para o programador isso não fará muita diferença, mas, para o cliente que comprou o seu aplicativo fará muita diferença, pois uma mudança de versão da maneira incorreta poderá fazer com que o cliente perca todos os dados que ele já possuía. Portanto, cuidado!

Temos a nossa tabela de usuário e agora precisamos inserir e recuperar usuários do banco de dados. Para inserir, não podemos usar o método execSQL para executar o código SQL diretamente, pois isso é muito perigoso. Caso fique utilizando execução direta de SQL, pode ser que alguém se aproveite disso para fazer uma injeção de SQL (SQL Injection), causando danos a sua base da dados. Por isso, devemos inserir usuários por meio de um objeto ContentValues que funciona como um HashMap. Após colocar os dados de um usuário em um ContentValues, basta inseri-lo passando-o como parâmetro de um getWritableDatabase().insert(). Veja o exemplo a seguir:

    public void inserir(Usuario usuario) {
        ContentValues values = new ContentValues();

        values.put(COL_NOME, usuario.getNome());
        values.put(COL_EMAIL, usuario.getEmail());

        getWritableDatabase().insert(TABELA, null, values);
    }


Inserimos o usuário e agora temos que recuperá-lo. Temos que posicionar um cursor para o local em que gostaríamos de recuperar os dados, então você usa um SELECT para pegar os dados que você quer, depois use o método moveToNext() para movimentar o cursor que irá se posicionar na próxima linha da tabela. Então, é só fazer um loop que irá percorrer todas as linhas da tabela. Vá colocando todos os dados em uma lista e, então, retorne-a. Veja o exemplo a seguir:

    public List<Usuario> getLista() {
        List<Usuario> lista = new ArrayList<>();

        String sql = "SELECT * FROM " + TABELA + ";";
        Cursor c = getReadableDatabase().rawQuery(sql, null);

        Usuario usuario = null;

        while(c.moveToNext()) {
            usuario = new Usuario();

            usuario.setId(c.getString(c.getColumnIndex(COL_ID)));
            usuario.setNome(c.getString(c.getColumnIndex(COL_NOME)));
            usuario.setEmail(c.getString(c.getColumnIndex(COL_EMAIL)));

            lista.add(usuario);
        }

        return lista;
    }


Além de inserir um usuário, é preciso deletar algum quando necessário. Por isso, também precisamos criar um método deletar que receba um usuário como parâmetro e o exclua do banco por meio da checagem de seu id. Vamos utilizar o método getWritableDatabase().delete para evitarmos uma sql injection. Ele receberá como parâmetro um array de strings. Neste caso, iremos passar o id do usuário para informar qual usuário é que deve ser excluído. Um exemplo de código pode ser visto a seguir:

    public void deletar(Usuario usuario) {
        String[] argumentos = {usuario.getId().toString()};
        getWritableDatabase().delete(TABELA, COL_ID + "=?", argumentos);
    }


Já inserimos, recuperamos e excluímos usuários do banco. Agora precisamos alterar ou dar um update no conteúdo de um determinado usuário que se encontra no banco. Para isso, precisamos novamente receber um usuário como parâmetro de um método alterar, que conterá todas as novas informações a serem alteradas em um usuário já inserido. A seguir segue um exemplo de código:

    public void alterar(Usuario usuario) {
        ContentValues values = new ContentValues();
        values.put(COL_NOME, usuario.getNome());
        values.put(COL_EMAIL, usuario.getEmail());

        String[] argumentos = {usuario.getId().toString()};
        getWritableDatabase().update(TABELA, values, COL_ID + "=?", argumentos);
    }


Pronto, agora a nossa classe já está bem funcional e pode ser utilizada em uma activity:

UsuarioDAO dao = new UsuarioDAO(MinhaActivity.this);
List<Usuario> usuarios = dao.getLista();


Suponha agora que você tenha que alterar a estrutura de banco de dados, aumentando mais um campo, que neste exemplo será o endereço do usuário. Para isso, precisamos adicionar o novo campo no onCreate para aqueles que irão instalar o aplicativo pela primeira vez e precisamos apenas atualizar a nova estrutura do banco no celular daqueles que já possuem o aplicativo instalado. No segundo caso, é preciso modificar o método onUpgrade(). Além disso, é imprescindível trocar a versão, que atualmente está 1, para o valor 2. Então, o onCreate ficará assim:

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "CREATE TABLE " + TABLE + " ("
            + COL_ID + " INTEGER PRIMARY KEY, "
            + COL_NOME + " TEXT UNIQUE NOT NULL, "
            + COL_EMAIL + " TEXT, "
            + COL_ENDERECO + " TEXT"
            + ");";
       
        db.execSQL(sql);
    }


Enquanto que o onUpgrade não apenas mais derrubará a tabela e a criará novamente. Ele agora será utilizado para alterar a tabela antiga, acrescentando mais uma coluna:

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql = "ALTER TABLE " + TABLE + " ADD COLUMN " + COL_ENDERECO + " TEXT;";

        db.execSQL(sql);
    }


Lembre-se também de adicionar o novo campo nos métodos inserir, getLista e alterar.

E se, por acaso, decidirmos alterar novamente o banco de dados? O banco irá para a versão 3, mas o que acontece com aqueles que instalaram o aplicativo e estão ainda com versões anteriores? Além de atulizar o método onCreate e alterar o inserir, getLista e alterar, é preciso tomar cuidado com o método onUpgrade. Ele ficará responsável para progressivamente realizar todas as atualizações desde a que está instalada no celular até chegar na atualização atual.

Suponha então que vamos adicionar o telefone do usuário. O onUpgrade ficaria assim:

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql;

        switch(oldVersion) {
            case 1:
                sql = "ALTER TABLE " + TABLE + " ADD COLUMN " + COL_ENDERECO + " TEXT;";
                db.execSQL(sql);
            case 2:
                sql = "ALTER TABLE " + TABLE + " ADD COLUMN " + COL_TELEFONE + " TEXT;";
                db.execSQL(sql);
        }
    }


Assim, se a versão antiga instalada for a 2, será feita a atualização do banco executando o que está no case 2. Se a versão antiga instalada for a 1, será feita a atualização do banco executando o que está tanto no case 1 quanto no case 2. Repare que eu não usei o break!

O código completo de nosso UsuarioDAO pode ser visto logo a seguir:

public class UsuarioDAO extends SQLiteOpenHelper {
    private static final String DATABASE = "appExemplo";
    private static final String TABELA = "Usuarios";
    private static final int VERSAO = 3;
    private static final String COL_ID = "id";
    private static final String COL_NOME = "nome";
    private static final String COL_EMAIL = "email";
    private static final String COL_ENDERECO = "endereco";
    private static final String COL_ENDERECO = "endereco";

    public UsuarioDAO(Context context) {
        super(context, DATABASE, null, VERSAO);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "CREATE TABLE " + TABLE + " ("
            + COL_ID + " INTEGER PRIMARY KEY, "
            + COL_NOME + " TEXT UNIQUE NOT NULL, "
            + COL_EMAIL + " TEXT, "
            + COL_ENDERECO + " TEXT, "
            + COL_TELEFONE + " TEXT"
            + ");";
      
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        String sql;

        switch(oldVersion) {
            case 1:
                sql = "ALTER TABLE " + TABLE + " ADD COLUMN " + COL_ENDERECO + " TEXT;";
                db.execSQL(sql);
            case 2:
                sql = "ALTER TABLE " + TABLE + " ADD COLUMN " + COL_TELEFONE + " TEXT;";
                db.execSQL(sql);
        }
    }

    public void inserir(Usuario usuario) {
        ContentValues values = new ContentValues();

        values.put(COL_NOME, usuario.getNome());
        values.put(COL_EMAIL, usuario.getEmail());
        values.put(COL_ENDERECO, usuario.getEndereco());
        values.put(COL_TELEFONE, usuario.getTelefone());

        getWritableDatabase().insert(TABELA, null, values);
    }

    public List<Usuario> getLista() {
        List<Usuario> lista = new ArrayList<>();

        String sql = "SELECT * FROM " + TABELA + ";";
        Cursor c = getReadableDatabase().rawQuery(sql, null);

        Usuario usuario = null;

        while(c.moveToNext()) {
            usuario = new Usuario();

            usuario.setId(c.getString(c.getColumnIndex(COL_ID)));
            usuario.setNome(c.getString(c.getColumnIndex(COL_NOME)));
            usuario.setEmail(c.getString(c.getColumnIndex(COL_EMAIL)));
            usuario.setEndereco(c.getString(c.getColumnIndex(COL_ENDERECO)));
            usuario.setTelefone(c.getString(c.getColumnIndex(COL_TELEFONE)));

            lista.add(usuario);
        }

        return lista;
    }

    public void deletar(Usuario usuario) {
        String[] argumentos = {usuario.getId().toString()};
        getWritableDatabase().delete(TABELA, COL_ID + "=?", argumentos);
    }

    public void alterar(Usuario usuario) {
        ContentValues values = new ContentValues();
        values.put(COL_NOME, usuario.getNome());
        values.put(COL_EMAIL, usuario.getEmail());
        alues.put(COL_TELEFONE, usuario.getTelefone());

        String[] argumentos = {usuario.getId().toString()};
        getWritableDatabase().update(TABELA, values, COL_ID + "=?", argumentos);
    }
}


That's all! ^^

quarta-feira, 7 de dezembro de 2016

Hello World em JSF2 no Eclipse passo a passo

Usou-se eclipse neon.1 e tomcat 8 para a criação deste hello world.

1) Crie um Dynamic web project (File -> New -> Dynamic web project).

O nome do projeto será JSF2HelloWorld. Já selecione o "Apache Tomcat v8.0" em Target runtime. Vá dando next até não poder mais. Na última tela marque a checkbox "Generate web.xml deployment descriptor". Clique em finish e o projeto estará criado.

Outra forma de adiconar o tomcat seria clicando com o botão direito em cima do projeto, indo em "Properties" > "Java Build Path" > "Add Library" > Selecionar "Server Runtime" e dar next e, então selecionaria o Tomcat. Mas para que ele possa aparecer em "Server Runtime" é necessário que ele tenha antes sido baixado da internet, seja descompactado (não precisa instalá-lo) e que seja indicado o seu caminho na aba "Servers" do Eclipse Java EE.

2) Baixando e configurando o JSF2.

Clique com o botão direito do mouse sobre o projeto criado. Vá em "Properties" e depois em "Project facets". Marque o checkbox "JavaServer Faces". Irá aparece abaixo "Further configuration required...". Clique nele. Ao abrir a tela "Modify Faceted Project" clique no botão Download library que tem o desenho de um disquete. Faça o download da JSF Mojarra, que é o criado pela Oracle e que serve de referência para as demais. Feito o download, coloque no URL Mapping Patterns: *.jsf e clique em OK.

Perceba agora que em seu projeto foi criado um arquivo chamado "faces-config.xml" e o seu "web.xml" foi criado com o seguinte código:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
  <display-name>JSF2HelloWorld</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/faces/*</url-pattern>
  </servlet-mapping>
  <context-param>
    <description>State saving method: 'client' or 'server' (=default). See JSF Specification 2.5.2</description>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value>
  </context-param>
  <context-param>
    <param-name>javax.servlet.jsp.jstl.fmt.localizationContext</param-name>
    <param-value>resources.application</param-value>
  </context-param>
  <listener>
    <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
  </listener>
</web-app>


3) Criando um arquivo xhtml

O JSF2 é utilizado junto com xhtml. Por isso, vamos criar um "helloWorld.xhtml". Clique com o botão direito sobre "WebContent" > "New" > "HTML File". Ponha o nome do arquivo de "helloWorld.xhtml", dê um "next" e escolha um template "New XHTML File (1.0 strict)" e clique em finish. Cole no seu xhtml o código abaixo:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:c="http://java.sun.com/jsf/core"
      xmlns:ui = "http://java.sun.com/jsf/facelets"
      xmlns:h = "http://java.sun.com/jsf/html">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Insert title here</title>
</head>
<h:body>

    <h:outputLabel value="Hello World"></h:outputLabel>   

</h:body>
</html>


Para que esse seu "helloWorld.xhtml" seja a primeira página a aparecer ao executar o projeto é necessário fazer uma pequena modificação no seu arquivo web.xml. Basta modificar o conteúdo da sua tag welcome-file, como no código abaixo:

  <display-name>JSF2HelloWorld</display-name>
  <welcome-file-list>
    <welcome-file>helloWorld.xhtml</welcome-file>
  </welcome-file-list>


Rode a sua aplicação pelo servidor e então veja o seu resultado pelo link:
http://localhost:8080/JSF2HelloWorld/faces/helloWorld.xhtml

Se você tiver posto URL Mapping Patterns: *.jsf na hora de configurar o jsf, então o link será:
http://localhost:8080/JSF2HelloWorld/index.jsf

O hello world de JSF2 está feito. Mas, caso queira produzir algo mais elaborado é preciso ter em mente os seguinte pontos:

1) Criando as Beans

As classes do java que irão de comunicar com as páginas jsf (xhtml) deverão estar no padrão Java Bean, ou seja:

- Variáveis de instância devem ser private e só podem ser acessadas por getters e setters.
- Exemplo: se a classe tiver um variável "String nome", deve ser criado um getNome() e setNome(). Se for um boolean, deve ser usado "is" em vez de "get", como em "isLigada()" para um variável "boolean ligada", por exemplo. Seguir esse padrão Java Bean para o uso do JSF é imprescindível!
- Deve ser criado um construtor sem parâmetros.
- A classe deve implementar java.io.Serializable.

Beans gerenciados (Managed beans) são beans que pode ser acessados a partir de uma página JSF. Devem ter um nome e um escopo. Existem alguns escopos, como "@ApplicationSocoped", "@SessionScoped", "@RequestScoped", entre outros. O escopo determina o tempo de existência da instância do objeto, se a instância irá existir durante toda a aplicação, ou apenas durante a sessão ou apenas o tempo de duração de uma requisição.

2) Mapeando a classe Bean

Após criada, a classe Bean ainda não pode ser acessada, mesmo que tenha seguido todos os padrões de nomenclatura. Para isso é necessário mapeá-la. O mapeamento pode ser feito pelo arquivo "faces-config.xml" ou via anotações (annotations).

Uma opção de anotação é a @Named que pertence ao pacote javax.inject.Named. Veja o exemplo abaixo:

@Named("usuario")
public class UsuarioBean implements Serializable { ... }


Outra opção é a anotação @ManagedBean do pacote javax.faces.bean. Se nenhum parâmetro for informado, o nome utilizado será o mesmo da classe, porém com a 1ª letra minúscula. Também podemos utilizar o parâmetro name para especificar um nome qualquer, como no exemplo abaixo:

@ManagedBean(name="usuario")
public class UsuarioBean implements Serializable { ... }


Veja abaixo um exemplo:

@ManagedBean
@ViewScoped
public class UsuarioBean implements Serializable {
    private int idade;
   
    public UsuarioBean() { }
   
    public int getIdade() { return this.idade; }
    public void setIdade(int idade) { this.idade = idade; }
    public void incrementaIdade() { this.idade = this.idade + 1; }
}


<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:h = "http://java.sun.com/jsf/html">
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<title>Exemplo JSF</title>
</h:head>
<h:body>
    Idade: #{usuarioBean.idade}
    <h:form>
        <h:inputText value="#{usuarioBean.idade}" />
        <h:commandButton value="Altera" />
        <h:commandButton value="Incrementa" action="#{usuarioBean.incrementaIdade}" />   
    </h:form>
</h:body>
</html>


A expression language #{usuarioBean.idade} serve tanto para pegar o valor da idade, como para mudar. Portanto, é usado tanto como um set e um get dependendo da ação que é feita. Se for só mostrar, funciona como um get, se for para enviar um valor por meio de uma submissão de uma requisição, então funciona como um set.

A expression language #{usuarioBean.incrementaIdade} serve para utilizar o método incrementaIdade que existe no objeto.

That's all.

quinta-feira, 17 de novembro de 2016

Uso de Singleton e Application customizado

Suponha que você tenha criado uma classe Livro, com o título, descrição, nome do autor e etc. Após isso tenha criado uma classe LivroControl que possuirá um ArrayList de Livros e que conterá os métodos necessários para o controle dessa lista de livros. Acontece que você deseja que essa lista seja única e acessível para toda a aplicação. O que você faz?

Os dois caminhos mais comuns são:
1) Instanciar um objeto LivroControl no método onCreate da classe Application
2) Usar o padrão Singleton

Caso 1: Instanciando LivroControl no Application

Application existe em todo aplicativo Android. É criado na abertura do aplicativo e, também possui um ciclo de vida, que é diferente de uma Activity. Mas, como vou alterar o meu Application?

Para alterá-lo é preciso criar uma classe filha e depois, basta sobrescrever o método onCreate, como no exemplo abaixo:

public class CustomApplication extends Application{

    private LivroControl livroControl;

    @Override
    public void onCreate() {
        super.onCreate();
        livroControl = new LivroControl();
    }

    public LivroControl getLivroControl() {
        return livroControl;
    }
}


Para usar esse CustomApplication no lugar do Application é necessário alterar um atributo xml chamado 'name' que está dentro da tag <application> do AndroidManifest.xml. Segue um exemplo:

    <application
        android:name=".application.CustomApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".ui.activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>


Feito isso, é possível acessar a CustomApplication de qualquer lugar:

final CustomApplication customApplication = getCustomApplication();
List<Livro> listaLivros = customApplication.getLivroControl().retrieveLista();


Caso 2: Usando o padrão Singleton


No padrão Singleton, o construtor de LivroControl deverá ser privado. Portanto, não poderá ser instanciado por alguém fora da classe. Quem fará a única instanciação será um método chamado getInstance, que instanciará o LivroControl apenas uma única vez. O método getInstance deve ser estático e thread-safe. Segue o código de exemplo:

public final class LivroControl {
    private final List<Livro> listaLivro = new ArrayList<>();
    private static ItemControl INSTANCE;

    private ItemControl() { }

    public static synchronized ItemControl getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ItemControl();
        }
        return INSTANCE;
    }
}


Também é possível fazer esse mesmo Singleton usando Enumerações. Basta jogar todos os métodos de LivroControl para dentro de uma Enum e os métodos do LivroControl apenas acessarão os métodos do Enum. Veja o exemplo baixo:

public final class LivroControl {
    public void adicionarLivro(Livro livro) {
        ListaLivros.INSTANCE.adicionarLivro(livro);       
    }

    public enum ListaLivros {
        INSTANCE;

        private static List<Livro> listaLivro;
        static {
            listaLivro = new ArrayList<>();
        }

        private ListaLivros() { }

        public void adicionarLivro(Livro livro) {
            listaLivro.add(livro);
        }
    }
}


Recomendo a leitura desta discussão caso queira se aprofundar no uso de Enumerações em Singletons:
What is an efficient way to implement a singleton pattern in Java?

sexta-feira, 4 de novembro de 2016

Criando um ListView customizado

Usa-se o ArrayAdapter para ListViews bem simples em que seja necessário exibir apenas TextViews que são preenchidos com o toString() do objeto. Enquanto que, para se criar ListViews complexas, é necessário desenvolver um adapter customizado. Mas, para isso, é importante conhecer como funciona a interação entre a ListView e o adapter.

A ListView pergunta ao adapter quantas linhas ele terá que exibir e solicita, para cada objeto da lista, sua representação visual, que retornará uma View preenchida com as informações do objeto.

Para criar um adapter customizado é preciso fazer uma classe que herde da classe BaseAdapter, que é a superclasse de todos os adapters do Android. Essa superclasse possui um construtor com dois atributos: um contexto e uma lista de objetos que se quer exibir. O contexto é usado para carregar os recursos da aplicação (layout, imagens...), além de guardar uma referência para a activity que é passada como parâmetro no construtor.

Ao criar um adapter, tem-se que implementar 4 métodos: getCount(), getItem(int), getItemId(int) e getView(int, View, ViewGroup).

getCount(): retorna a quantidade de linhas que a ListView exibirá (basta pegar o tamanho da lista de um determinado objeto);

Ex.
@Override
public int getCount() { return listaDeObjetos.size(); }


getItem(int): retorna um objeto, da lista de objetos, com base em sua posição;

Ex.
@Override
public Object getItem(int posicao) { return listaDeObjetos.get(posicao); }


getItemId(int): retorna um identificador único de um objeto da lista, como um id;

Ex.
@Override
public long getItemId(int posicao) { return posicao; }


getView(int, View, ViewGroup): método que gera a View que representará cada item da lista; esse processo é feito em 4 etapas:

1) Obter o objeto pela posição passada como parâmetro;
2) Carregar layout;
3) Preencher os componentes com os atributos do objeto;
4) Retornar layout com os componentes devidamente preenchidos com as informações do objeto.

Ex.
public View getView(int position, View convertView, ViewGroup parent) {
    // 1 passo
    MeuObjeto meuObjeto = listaObjetos.get(position);
    // 2 passo
    View linha = LayoutInflater.from(contexto).inflate(R.layout.item_lista, null);
    // 3 passo
    TextView nomeObjeto = (TextView) linha.findViewById(R.id.nome_objeto);
    nomeObjeto.setText(meuObjeto.getNome());
    // 4 passo
    return linha;
}


Para carregar o arquivo de layout, utiliza-se a classe LayoutInflater, que, ao chamar o método inflate(int, Viewgroup) retorna o elemento-raiz do mesmo. Com esse objeto, consegue-se obter a referência dos componentes que ele contém usando o findViewById(int). Uma vez tendo as instâncias dos componentes, basta preencher cada um com as informações do objeto.

O layout da linha pode ser feito da maneira que você quiser. No exemplo acima, o "R.layout.item_lista" só possuia um único TexView, que foi vinculado ao TextView nomeObjeto e depois setado pelo setText. Mas, poderia ter sido criado um layout com uma ImageView e mais dois TextViews, por exemplo. Então, no passo 3, a imagem e os TextViews deveriam ser setados, para que a view linha seja retornada completamente preenchida.

Como a listView só mostra os itens que cabem na tela, não é necessário instanciar todas as views existentes para cada item da lista, apenas as que estão sendo mostradas. Além disso, as views que foram mostradas, mas que, após os rolamento da lista, deixaram de ser mostradas, podem ser utilizadas novamente para apresentar novos itens, sendo necessário apenas uma atualização de seus dados. É para isso que existe o segundo parâmetro do getView: "View convertView". Esse parâmetro é passado pelo Android e faz referência a view que foi instanciada e visualizada na tela, mas que agora não está mais visível devido ao rolamento da listView. Segue um código de utilização desse parâmetro:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View linha;
       
        if(convertView == null) {
            linha = activity.getLayoutInflater().inflate(R.layout.item_lista, parent, false);
        } else {
            linha = convertView;
        }


    TextView nomeObjeto = (TextView) linha.findViewById(R.id.nome_objeto); 
        MeuObjeto meuObjeto = (MeuObjeto) getItem(position);
        nomeObjeto.setText(meuObjeto.getNome());

        return linha;
    }


Feito o adapter customizado, tem-se que utilizá-lo como parâmetro de criação de uma listView. Para isso, basta colocar no onCreate da activity um código parecido com esse:

ListView listView = new ListView(this);
MeuAdapter adapter = new MeuAdapter(this, ListaObjetos);
listView.setAdapter(adapter);


Sempre que a lista de objetos do adapter for modificada, no caso de um acréscimo de um novo item, por exemplo, é necessário informar à listView que isso ocorreu. Para isso, usa-se o método notifyDataSetChanged(). Veja abaixo um exemplo de uso desse método:

    public void setItems(List<MeuObjeto> novaLista) {
        listaObjetos.clear();
        listaObjetos.addAll(novaLista);

        notifyDataSetChanged();
    }


Esse método setItems pode ser criado dentro do adapter. Assim, sempre que se queira modificar a lista de objetos do adapter, basta utilizá-lo, pois ele limpa uma lista de objeto e adiciona uma nova. Ou seja, se a lista foi modificada ao ser adicionado um novo item, a antiga lista será limpa e substituída pela novaLista. Ao final, informa-se à listView que os dados foram modificados pelo notifyDataSetChanged.

Sugestão de leitura: Utilizando o padrão ViewHolder.

quarta-feira, 2 de novembro de 2016

O que significa o atributo xmlns em códigos XML?

Para uma introdução às características do XML, sugiro que leia a postagem "Quais as diferenças entre XML e HTML ?". Agora vamos ao entendimento do atributo 'xmlns'.
Veja esse código XML:

<table>
  <tr>
    <td>Melancia</td>
    <td>Carambola</td>
  </tr>
</table>


Agora veja esse outro código XML:

<table>
  <name>Mesa de Café Africana</name>
  <width>80</width>
  <length>120</length>
</table>


O primeiro código utiliza a tag <table> para se referir a uma tabela com suas linhas e colunas, enquanto que o segundo código utiliza a tag <table> para se referir às características de uma mesa. Dessa maneira, pode ser que ocorra conflito entre esses códigos por possuírem tags idênticas que se relacionam a conteúdos diversos. Por isso, faz-se necessário o uso de prefixos, como no código abaixo:

Primeiro código com prefixo:

<h:table>
  <h:tr>
    <h:td>Melancia</h:td>
    <h:td>Carambola</h:td>
  </h:tr>
</h:table>


Segundo código com prefixo:

<f:table>
  <f:name>Mesa de Café Africana</f:name>
  <f:width>80</f:width>
  <f:length>120</f:length>
</f:table>


Após o uso dos prefixos 'h' e 'f' os códigos acima ficam livres de sofrer conflitos. Entretanto, faz-se necessário o uso do atributo 'xmlns' para identificar os atribuitos usados. Quando um 'xmlns' é definido para um prefixo, todos os elementos filhos com o mesmo prefixo terão o mesmo identificador (namespace). Usa-se como identificador uma Uniform Resource Identifier (URI) que é um conjunto de caracteres que representa uma fonte na internet. O propósito da URI é dar um nome único ao identificador. Uma URI pode ser uma URL (Uniform Resource Locator) por exemplo. Veja o próximo exemplo:

<root
xmlns:h="http://www.w3.org/TR/html4/"
xmlns:f="http://www.w3schools.com/furniture">

<h:table>
  <h:tr>
    <h:td>Melancia</h:td>
    <h:td>Carambola</h:td>
  </h:tr>
</h:table>

<f:table>
  <f:name>Mesa de Café Africana</f:name>
  <f:width>80</f:width>
  <f:length>120</f:length>
</f:table>
</root>


Para não ter que usar o prefixo em todos os elementos filhos, é possível criar um xmlns padrão. Assim, todas as tags que não estiverem prefixas assumirão automaticamente o identificar padrão. Como no código abaixo:

<table xmlns="http://www.w3.org/TR/html4/">
  <tr>
    <td>Melancia</td>
    <td>Carambola</td>
  </tr>
</table> 

Quais as diferenças entre XML e HTML ?

O XML é uma linguagem de marcação, assim como o HTML, entretanto possui diferentes objetivos e características. Abaixo listo as diferenças entre essas linguagens para que possa ficar mais claro.

  • Quanto aos objetivos da linguagem...
XML: feito para carregar/transportar dados com foco no que o dado representa.
HTML: feito para apresentar os dados visualmente com foco no visual.

  • Quanto a pré-definição de tags e estrutura...
XML: não possui tags e nem estrutura pré-definidas, ou seja, o desenvolvedor é que inventa suas próprias tags e estrutura.
HTML: possui tags pré-definidas, como <h1> ou <img> e deve seguir um padrão, por exemplo, as tags <head> e <body> devem estar dentro da tag <html>.

  • Quanto às tags de fechamento...
XML: sempre deve existir tag de fechamento.
HTML: algumas tags funcionam bem mesmo sem tag de fechamento.

  • Quanto ao "case sensitive"...
XML: as tags são case sensitive.
HTML: não são.

  • Quanto ao aninhamento de tags...
XML: tags devem ser aninhadas corretamente. ex. <b><i>texto</i></b>
HTML: pode funcionar mesmo com aninhamento incorreto. ex. <b><i>texto</b></i>

  • Quanto ao truncamento de espaços em branco...
XML: não faz truncamento de espaços em branco, ou seja, 3 espaços em branco continuam sendo 3 espaços em branco.
HTML: faz o truncamento de espaço em branco, ou seja, 3 espaços em branco se transformam em um único espaço em branco.

Outras características do XML:
  • É auto-descritivo. As tags informam o que significa cada dado.
  • XML não faz nada. Caso se queira utilizar essa informação, deve-se criar algum software que leia, carregue, armazene e mostre as informações com base nas tags.
  • É extensível. Se adicionar novas ou remover antigas tags, aplicações antigas ainda funcionarão.
  • Simplifica o compartilhamento, o transporte, a mudança de plataformas e a disponibilidades dos dados.
  • Armaneza dados em formato texto e isso gera uma indepedência de software e de hardware com relação a armanezamento, transporte e compartilhamento de dados.
  • Assim como o HTML, o XML também segue uma estrutura de árvore.
  • O XML prolog, ou seja, o código <?xml version="1.0" encoding="UTF-8"?> é opcional, mas, se for colocado, deve ser posto logo no início.
  • O valor dos atributos sempre deve possuir aspas. Ex. <note date="12/11/2007">, ficaria incorreto caso as aspas não fossem usadas, como a seguir <note date=12/11/2007>.
  • Elementos vazio são tags sem conteúdo. Elementos vazio pode ser expressos de duas maneiras: 1. <element></element> ou 2. <element />.

terça-feira, 1 de novembro de 2016

Como aplicar uma Toolbar em seu app

Antes de utilizar uma Toolbar, é importante conhecer o funcionamento de uma ActionBar. Feito isso, pode continuar a leitura desta postagem.

É aconselhável usar uma Toolbar em vez de uma ActionBar padrão, ou seja, usar uma Toolbar como ActionBar. Isso se deve por conta da Toolbar ser compatível com um maior número de versões android e, além disso, a ActionBar pode variar seu padrão de funcionamento dependendo da versão android que se está usando.

Passos para se utilizar uma Toolbar:
1) Activity deve herdar da AppCompatActivity;
2) No manifest, o <application> deve usar como theme um "...NoActionBar"

<application
    android:theme="@style/Theme.AppCompat.Light.NoActionBar"
    />


3) No layout xml da activity, deve-se criar um elemento toolbar como no código abaixo:

<android.support.v7.widget.Toolbar
   android:id="@+id/my_toolbar"
   android:layout_width="match_parent"
   android:layout_height="?attr/actionBarSize"
   android:background="?attr/colorPrimary"
   android:elevation="4dp"
   android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
   app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>


4) No onCreate() da activity, deve-se chamar o método setSupportActionBar(), passando-se como parâmetro a sua toolbar criada, como no código abaixo:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my);
    Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
    setSupportActionBar(myToolbar);

    }


Obs. O uso do setSupportActionBar(myToolbar) deve ser feito após o setContentView(R.layout.activity_my), caso contrário o título e os botões da toolbar não aparecem. Além disso, deve-se colocar <item name="windowActionBar">false</item> no style que está sendo usado.