Utilizando o padrão ViewHolder
Quando vamos fazer um aplicativo Android, inúmeras vezes será necessário utilizar uma lista, o Alex mostrou para gente como criar uma lista e personaliza-la usando ListView
.
Depois que fazemos todo esse procedimento vemos que a listagem às vezes engasga quando estamos passando os seus itens, vamos entender porque isso acontece. Vamos analisar esse Adapter
:
public class AlunoAdapter extends BaseAdapter {
private Context context;
private List<Aluno> alunos;
public AlunoAdapter(Context context, List<Aluno> alunos) {
this.context = context;
this.alunos = alunos; }
@Override public int getCount() { return alunos.size(); }
@Override public Object getItem(int position) { return alunos.get(position); }
@Override public long getItemId(int position) { return alunos.get(position).getId(); }
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view = LayoutInflater.from(context) .inflate(R.layout.item_aluno, parent, false);
TextView nome = (TextView) view.findViewById(R.id.item_nome);
TextView email = (TextView) view.findViewById(R.id.item_email);
Aluno aluno = (Aluno) getItem(position);
nome.setText(aluno.getNome());
email.setText(aluno.getEmail());
return view; } }
Bom, nada de novo até agora, mas nesse momento estamos com um pequeno problema, quando nossa lista está com muitos itens ela trava, por quê ?
Entendendo o processo de criação entre a ListView e o Adapter
O ListView
quando é criado, pergunta ao Adapter
qual é a quantidade de itens que ele terá, e recebe a resposta atráves do método getCount()
.
Quando recebe a quantidade ele começa a se preparar para exibir os itens, vendo a quantidade que caberá na tela, vamos imaginar que sejam apenas 5 itens qua possam ser exibidos, nisso o Android vai inflar esses 5 itens e vai devolver para o ListView
através do método getView()
, além disso o Android acaba inflando mais duas views, para reaproveitarmos.
Está view que é criada para reaproveitamento é a view que recebemos como parâmetro no método getView()
, chamamos ela de convertView
:
Contudo se olharmos nosso código em momento algum estamos utilizando a convertView
, para cada view que o ListView
pede estamos inflando uma nova, por esse motivo a lista começa a engasgar, devido a falta de memória.
Refatorando o adapter para reaproveitar a view
Vamos então refatorar nosso código para deixar nossa ListView
com uma boa performance. Para isso precisamos saber se já temos uma view pronta para ser reaproveitada, caso tenhamos nós iremos utiliza-la, caso contrário teremos que inflar uma nova.
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view;
if( convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item, parent, false); }
else { view = convertView; }
TextView nome = (TextView) view.findViewById(R.id.item_nome);
TextView email = (TextView) view.findViewById(R.id.item_email);
Aluno aluno = (Aluno) getItem(position);
nome.setText(aluno.getNome());
email.setText(aluno.getEmail());
return view;
}
Agora estamos reaproveitando a view da forma que queríamos, entretanto ainda podemos melhorar bastante a questão de performance.
Como estamos reaproveitando, nós já conhecemos todas as view que aquele item possui, mas ainda assim estamos fazendo findViewById()
para cada view.
O findViewById()
percorre o xml em busca da view, perguntando para cada view se ela possui aquele id, indo da view principal descendo sobre seus "filhos" da esquerda para a direita, como se fosse uma árvore genealógica.
Isso num layout mais complexo levaria um certo tempo para encontrar cada view certinha e popula-la, por isso precisamos dar um jeito de fazermos a busca apenas quando criamos o item e quando reaproveitarmos o item criado, também aproveitar as buscas.
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view;
TextView nome TextView email;
if( convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
nome = (TextView) view.findViewById(R.id.item_nome);
email = (TextView) view.findViewById(R.id.item_email); }
else { view = convertView; }
Aluno aluno = (Aluno) getItem(position);
nome.setText(aluno.getNome());
email.setText(aluno.getEmail());
return view; }
Dessa forma resolvemos o problema! Pena que o compilador está reclamando, pois quando formos reaproveitar uma view não teremos referência para os TextView
, e agora?
Implementando o ViewHolder
Bom, para resolvermos esse problema existe um design pattern chamado ViewHolder
que irá segurar as informações da view.
Para utilizarmos esse pattern iremos criar uma classe que precisará da view que ela pegará as informações.
public class ViewHolder {
final TextView nome; final TextView email;
public ViewHolder(View view) {
nome = (TextView) view.findViewById(R.id.item_nome);
email = (TextView) view.findViewById(R.id.item_email);
}
}
Legal, classe criada, está faltando apenas a usarmos. Bora fazer isso !
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder holder;
if( convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
holder = new ViewHolder(view); } else {
view = convertView; }
Aluno aluno = (Aluno) getItem(position);
holder.nome.setText(aluno.getNome());
holder.email.setText(aluno.getEmail());
return view; }
Poxa, parece tudo certo, entretanto ainda estamos sendo barrados pelo compilador. O mesmo erro que estávamos lendo antes, quando formos reaproveitar a view o ViewHolder
não será criado, portanto não terá referência para ser usado.
Para resolver isso, precisamos deixar o ViewHolder
"pendurado" na view que ele pertence, dessa forma, quando formos reaproveitar a view, conseguiremos o recuperar.
@Override public View getView(int position, View convertView, ViewGroup parent) {
View view; ViewHolder holder;
if( convertView == null) {
view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);
holder = new ViewHolder(view);
view.setTag(holder); } else {
view = convertView;
holder = (ViewHolder)
view.getTag(); }
Aluno aluno = (Aluno) getItem(position);
holder.nome.setText(aluno.getNome());
holder.email.setText(aluno.getEmail());
return view;
}
Conhecendo a API RecyclerView
Embora tenhamos implementado a ListView
considerando todos os detalhes que vimos neste post, repare que tivemos que ter o conhecimento do ViewHolder
para implementá-lo. Em outras palavras, se não o conhecêssemos, ainda teríamos o problema de performance conforme a lista fosse crescendo...
Pensando justamente nesse e outros detalhes, a Google disponibilizou uma API mais inteligente para a criação de listas que já nos obriga a realizar todas as implementações que vimos no Adapter
e na ListView
, e também, a implementação do ViewHolder
!
Essa é a API RecyclerView
que é explicada com mais detalhes nesse post que escrevi! Recomendo fortemente a leitura, pois é a forma mais adotada pelos desenvolvedores Android para criação de lista nas Apps.
Resumo
Como vimos, para termos um ListView
com boa performance, precisamos reutilizar as views que o Android cria a mais para gente, só para lembrarmos o nome que damos para essa view é convertView
.
Além disso para melhorar de vez a performance, podemos evitar que ele faça findViewById
s desnecessários, utilizamos o padrão ViewHolder
que irá cuidar de fazer isso para nós e associamos ele a view cujo ele pertence, desta forma otimizamos bastante o funcionamento da lista, agora nada de ve-lá engasgar! :D
Quer ver mais dicas bacanas sobre Android? Aqui na Alura temos vários cursos de Android, se você preferir presencial na Caelum temos dois cursos bem bacanas!