quinta-feira, 24 de março de 2011

Adapter Eficiente no Android

Olá povo,

Para preencher diversos componentes de UI como: ListView, Spinner (combo box) e GridView, o Android utiliza o conceito de Adapters. Esse é um padrão de projeto muito útil, e serve (como o próprio nome diz) para adaptar duas interfaces incompatíveis. No caso do Android, esse padrão serve para adaptar uma lista de objetos para uma lista elementos de interface gráfica, como as linhas de uma ListView.
Por exemplo, imagine uma lista de objetos da classe Pessoa, e você quer exibi-los na tela. O Android, através da classe ListView (que é um componente que exibe os componentes em forma de lista) solicita, para cada linha da lista um novo objeto View (que é a classe mãe de todos os elementos visuais do Android). Cabe ao Adapter, criar um objeto View que represente visualmente o objeto da posição específica da lista de objetos. Vamos detalhar mais como isso funciona na prática.
Crie um novo projeto no Eclipse marcando a opção para criar uma activity e nomeando-a como TelaListaActivity. Antes de mexer na Activity vamos criar a classe de objetos que queremos listar. No nosso caso, será a classe Pessoa que citamos acima.
public class Pessoa {
  private long id;
  private String nome;
  private String estado;

  public Pessoa(long id, String nome, String estado) {
     this.id = id;
     this.nome = nome;
     this.estado = estado;
  }

  public long getId() {
     return id;
  }
  public void setId(long id) {
     this.id = id;
  }
  public String getNome() {
     return nome;
  }
  public void setNome(String nome) {
     this.nome = nome;
  }
  public String getEstado() {
     return estado;
  }
  public void setEstado(String estado) {
     this.estado = estado;
  }
}

A classe acima não passa de um POJO (Plain Old Java Object), ou seja, apenas uma classe com métodos gets e sets. Agora vamos criar o arquivo de layout que representará cada linha a ser exibida na ListView. Crie o arquivo linha.xml dentro do diretório res/layout e deixe-o conforme abaixo:
<LinearLayout android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
   android:id="@+id/textView1"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:textsize="26dp"
   android:textcolor="#00FF00"/>
<TextView
   android:id="@+id/textView2"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
</LinearLayout>

Nosso layout também é bem simples. Consta apenas de um LinearLayout com dois TextViews, onde deixamos primeiro com o texto um pouco maior que o segundo e com a cor verde. Vamos agora criar nosso adaptador. Crie uma nova classe chamada PessoaAdapter e deixe-a conforme abaixo.
public class PessoaAdapter extends BaseAdapter {

   private Context ctx;
   private List<Pessoa> pessoas;

   public PessoaAdapter(Context ctx, 
      List <Pessoa> pessoas) {
      this.ctx = ctx;
      this.pessoas = pessoas;
   }

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

   @Override
   public Object getItem(int position) {
      return pessoas.get(position);
   }

   @Override
   public long getItemId(int position) {
      return pessoas.get(position).getId();
   }

   @Override
   public View getView(int position, View convertView,
      ViewGroup parent) {
      
      Pessoa p = pessoas.get(position);

      View v = LayoutInflater.from(ctx).inflate(
         R.layout.linha, null);

      TextView txt1 = (TextView) v.findViewById(
         R.id.textView1);
      TextView txt2 = (TextView) v.findViewById(
         R.id.textView2);

      txt1.setText(p.getNome());
      txt2.setText(p.getEstado());

      return v;
   }
}

Nossa classe herda BaseAdapter e tem dois atributos que são inicializados no construtor da classe. O atributo da classe Context será utilizado para carregar o arquivo de layout que acabamos de criar. Já o segundo é a lista de objetos da classe Pessoas que queremos adaptar.
Quando criamos um adapter, devemos implementar quatro métodos: getCount, getItem, getItemId e getView. O primeiro retorna a quantidade de linhas que o adaptador representa, se estamos adaptando a lista de pessoas, basta retornar o tamanho da lista de pessoas.
O método getItem serve para acessarmos o objeto que o adaptador está representando. No nosso caso, basta retornar o objeto da posição da lista.
Já o método getItemId serve pra retornar um identificador do objeto da posição passada como parâmetro. Esse método é opcional, mas como temos um atributo chamado id na classe pessoa, ele passa a ser um bom candidato. Então obtemos o objeto da classe Pessoa da posição especificada pelo parâmetro e retornamos o seu id.
O último método é o mais importante. É ele que vai pegar um objeto da lista de pessoas, carregar o arquivo de layout e atribuir os valores dos atributos do objeto pessoa para a view que representará a linha da lista.
Para carregar o arquivo de layout, utilizamos a classe LayoutInflater. Uma vez que esse layout é carregado, é retornado um objeto View que representa a árvore de Views definida no arquivo XML, que no nosso caso, tem um LinearLayout e dentro dele, dois TextViews. Uma vez de posse desse objeto View, obtemos as referências para os TextViews para podemos passar os valores dos atributos da classe Pessoa para eles. No final retornamos a View que carregamos e modificamos.

Vamos agora para atividade da aplicação. Nela vamos criar uma lista de objetos pessoa, criar o uma instância do nosso adapter e defini-lo como "preenchedor" da lista.
public class TelaListaActivity extends ListActivity {

   @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      ArrayList<Pessoa> pessoas = 
         new ArrayList<Pessoa>();

      pessoas.add(
         new Pessoa(1, "Nelson", "Pernambuco"));
      pessoas.add(
         new Pessoa(2, "Glauber", "São Paulo"));
      pessoas.add(
         new Pessoa(3, "José", "Minas Gerais"));
      pessoas.add(
         new Pessoa(4, "João", "Rio de Janeiro"));
      pessoas.add(
         new Pessoa(5, "Silvio", "Pernambuco"));
      pessoas.add(
         new Pessoa(6, "Marta", "Pernambuco"));
 
      PessoaAdapter adapter = 
         new PessoaAdapter(this, pessoas);

      setListAdapter(adapter);
   }
}

Se executarmos nossa aplicação teremos um resultado similar à figura abaixo:

Muito bom! Nossa lista de objetos pessoa foi exibido perfeitamente na ListView que é criada automaticamente quando herdamos da classe ListActivity. Mas vamos recaptular um pouco... O método getView é chamado para cada linha da linha. Então se tivermos 10000 linhas na lista carregaremos 10000 layouts? Pois é, e isso não é bom. Mas não se desespere, os engenheiros da Google criaram uma forma padrão de otimizar essas listas. Altere o método getView para que fique conforme abaixo:
@Override
public View getView(int position, View convertView, 
   ViewGroup parent) {
  
   ViewHolder holder;

   Pessoa p = pessoas.get(position);

   if (convertView == null){
      convertView = LayoutInflater.from(ctx).inflate(
         R.layout.linha, null);
 
      holder = new ViewHolder();
      holder.txtNome = (TextView) 
         convertView.findViewById(R.id.textView1);
      holder.txtEstado = (TextView) 
         convertView.findViewById(R.id.textView2);
      convertView.setTag(holder);
   } else {
      holder = (ViewHolder)convertView.getTag();
   }
   holder.txtNome.setText(p.getNome());
   holder.txtEstado.setText(p.getEstado());

   return convertView;
}

static class ViewHolder {
   TextView txtNome;
   TextView txtEstado;
}

A classe ViewHolder vai manter as referências para a Views filhas para evitar chamadas desnecessárias ao método findViewById para cada linha. O parâmetro convertView representa uma linha pode ser reusada, uma vez que nem todas as linhas podem estar sendo vistas na tela. Quando convertView é diferente null, podemos reusá-la, então não será necessário carregar uma nova instância do layout. Se ela for nula, carregamos o layout e instanciamos um novo ViewHolder para armazenar as Views que iremos alterar, e em seguida setamos esse ViewHolder na propriedade tag (que pode armazenar qualquer objeto) da convertView.

O resultado para o número de registros que usamos não é aparente, mas tente adicionar 10000 objetos rodar a lista com o primeiro e o segundo exemplo :) Façam esse teste usando a instrução abaixo pra alimentar a lista;

for (int i = 0; i < 10000; i++) {
   pessoas.add(new Pessoa(i, "Pessoa "+ i, "Acre"));  
}


Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

Fonte: Android Developers e Google IO 2010

22 comentários:

thiago silva disse...

Parabéns glauber pela ótima dica.

Jonas disse...

Bacana amigo,

Me ajudou a entender a função dos adapters...

Valeu!

Jhonathan Brandão disse...

Muito bacana seus artigos, cada vez que venho pelo seu blog as coisas ficam mais claras!

Seu blog está sendo uma das melhores fontes de pesquisa para desenvolver o pequeno aplicativo do meu TCC.

Parabéns.

Anônimo disse...

Ola, fiz o exemplo igual porem acontece esse erro:


01-29 01:35:51.619: E/AndroidRuntime(2491): Caused by: java.lang.RuntimeException: Your content must have a ListView whose id attribute is 'android.R.id.list'

Nelson Glauber disse...

Olá Anônimo,

Mais atenção... Na activity, você deve ter deixado a chamada pro método setContentView(R.layout.main).
Remova essa linha, como está no post, e vai funcionar.

Qualquer dúvida, olha esse post:
http://nglauber.blogspot.com/2010/02/listactivity-e-emptyview.html

4br4ç05,
nglauber

Anônimo disse...

teria como colocar os valores na horizontal tipo uma tabela os valores na frente do outro????
muito bom pelo poste vlw

Nelson Glauber disse...

Oi Anônimo,

Dá sim, basta você mudar o seu artigo de layout.
Para fazer algo do estilo DBGrid do Delphi seria um pouquinho mais complicado, mas ideia continuaria no arquivo de layout + adapter.

4br4ç05,
nglauber

Danilo Avner disse...

Nelson, eu tenho uma aplicação que usa o adapter. Só que eu estou com problemas.No meu caso ele mostra o nome no (txtview nome_pro), o codigo (txtview cod_pro) que estão dentro de um table row. Eu queria colocar um botão em cima do table row. Mas quando eu coloco, o produtoAdapter repete todo o layout. Assim o resultado fica que para cada linha no table row aparece um botão em cima, o nome, o codigo abaixo. Eu acho que é alguma coisa nessa linha aqui:

View v = inflater.inflate(R.layout.lista_pro, null)

No meu entendimento esse segundo parâmetro é exatamente que se diz
qual grupo que se deve repetir, se estiver nulo ele repete o layout
inteiro. Será que tem como indicar qual o layout e dizer só o grupo para repetição, para ele não repetir o layout inteiro???

Nelson Glauber disse...

Oi Danilo,

O comportamento descrito por você, é o esperado. Se você colocou o botão no arquivo de layout que está sendo usado pelo Adapter ele será repetido a cada linha sim.
Se você quiser que o botão apareça apenas uma vez, você deve criar um arquivo de layout para a tela (além do criado para o Adapter) colocando o botão e o listview. O único detalhe é que esse ListView TEM QUE TER o ID android:id="@android:id/list", o resto é um arquivo de layout normal.
Ex.:
[LinearLayout][Button][ListView][/LinearLayout]

Qualquer dúvida é só falar.

P.S.: O segundo paâmetro do inflate é a View onde eu quero carregar o layout. No seu caso é null mesmo...

4br4ç05,
nglauber

Williams disse...

Parabéns, Ótimo post.
Mas estou com um probleminha, é o seguinte...
Eu quero colocar um footer fixo na lista, tipo o botão e o comentar do app do facebook quando você vai comentar algo.
Ou seja, a lista normal como você fez e um campo texto fixo no footer.

Estou a um tempo tentando fazer isso mas sem sucesso :/

Nelson Glauber disse...

Oi Williams,

Se você quer que esse item fixo não faça parte da lista, basta colocá-lo fixo mesmo no layout. Ou seja, será mais um componente na tela que fará parte do seu arquivo de layout. Dá uma olhada aqui:
http://nglauber.blogspot.com.br/2012/08/cuide-da-sua-asynctask.html

Se quiser que ele seja um item da lista, basta colocar que o getCount() retorna o tamanho da lista + 1, e no getView(), você vê se a position == tamanho da lista. Em caso positivo, você cria a View fixa e retorna.
Uma boa prática é você implementar os métodos:
getViewTypeCount (que no seu caso retornaria 2) e getItemViewType (onde se position == tamanho da lista, retornaria 0, por exemplo e 1 caso contrário).

Espero ter ajudado, qualquer dúvida é só falar.

4br4ç05,
nglauber

Unknown disse...

Pois é... Parece quem ninguém testou o código que fato! cade o método setListAdapter?

Nelson Glauber disse...

oi anderson,

Se você prestar atenção, vai notar que está na linha 26 da classe TelaListaActivity.

4br4ç05,
nglauber

Joeldcamargo disse...

Olá Glauber.

Gostei muito do seu post.

Você poderia me tirar uma dúvida?

Eu tenho uma classe com uma variável "rs" de um ResultSet.

Este ResultSet me traz duas colunas, eu consigo utiliza-lo no exemplo abordado?

Abraço.

Nelson Glauber disse...

Oi Joel,

O objetivo do Adapter é carregar um arquivo de layout, preenchê-lo e retorna-lo para o componente que o está usando (por exemplo, um ListView).
Então, independente do objeto que você está usando, você pode usar o adapter, uma vez que ele "adapta" um objeto para ser exibido em um componente na tela.

4br4ç05,
nglauber

Anônimo disse...

Excelente, parabens!!! muito bem explicado. Como sugestão, gostaria de colocar a utilização de botões na listview, mais epecificamente com a função de exclusão da propria linha

Nelson Glauber disse...

Oi Anônimo,

Obrigado pelo elogio, mas para exclusão de itens da lista, sugiro dar uma olhada nesse post aqui: http://nglauber.blogspot.com.br/2013/07/listview-com-selecao-multipla-actionbar.html

4br4ç05,
nglauber

César Oliveira disse...

Mais uma matéria excelente.

Clara, objectiva e até parece fácil

Durante um tempo tive muita dificuldade para perceber os adapters.

Esta matéria reforçou mais o meu conhecimento sobre adepter.

Muito Obrigado

Anônimo disse...

Parabéns pelo post, gostaria de saber como funciona no caso de por exemplo modificar o texto de um elemento da lista. No caso para quando der scroll na tela não perder o texto setado. Obrigado

Nelson Glauber disse...

Oi Anônimo,

O componente visual (que nesse exemplo é o ListView) apenas reflete o conteúdo de uma lista de objetos por meio de um adapter. Sendo assim, caso você altere algum objeto da lista, ou a lista em si (adicionando ou removendo elementos), você precisa avisar ao adapter que esses dados foram modificados. Isso é feito por meio do método notifyDatasetChanged().

4br4ç05,
nglauber

Anônimo disse...

Porque minha listview, quando eu desço pra ver mais resultados, ela sobrescreve os dados com o que estava no começo?

Nelson Glauber disse...

olá,

O ListView reflete o conteúdo da lista armazenada no adapter.
À medida que você faz o scroll, a View que estava sendo exibida é desalocada e uma nova View é criada.
Para que o valor não seja perdido é preciso armazenar os valores desejados no adapter.


4br4ç05,
nglauber