Utilizando o padrão ViewHolder

(Última atualização em: 22 de fevereiro de 2017)

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 :

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 findViewByIds 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! 😀

Quer ver mais dicas bacanas sobre Android? Aqui no Alura temos vários cursos de Android, se você preferir presencial na Caelum temos dois cursos bem bacanas!

  • Romulo H. P. Amendola

    Excelente post, bem explicativo e produtivo! (Y)

    • Matheus Brandino

      Romulo, tudo bem ?

      Fico contente que tenha curtido !

      Agora é só por a mão na massa e adotar esse padrão para o desenvolvimento 🙂

      Abraços
      Matheus

  • Romulo H. P. Amendola

    !

  • Rodrigo Barro

    Show de bola Matheus, parabens pelo conteudo. Essa é a idéia basica do RecyclerView?

    • Matheus Brandino

      Oi Rodrigo !

      Então cara, por trás dos panos o RecyclerView já está fazendo isso para nós, um dos motivos da sua performance ser superior à do ListView.

      Tem um post meu aqui no alura também que você pode ver a idéia e como implementar o RecyclerView.

      Abraço ! 😀

  • gabriel

    Eu Tava com uma duvida monstra depois deste post…

    Estou liberto….

    KKK

    • Matheus Brandino

      Que coisa boa cara !!!

      Agora vamos seguir ai e continuar o aprendizado !!!

      Abraços 😀

  • newton altnetter

    Muito bom, me ajudou muito… obrigado

    • Matheus Brandino

      Não há de que !!!

      Fico feliz em poder ajudar ! Agora só seguir com os estudos 😀

  • Yuri Cavalcante

    Excelente, gostei porque explica mais a fundo o porquê de fazer tal coisa e não apenas mostra. Muito bom, continue assim.

  • Pingback: Personalizando uma ListView no Android - Blog da Alura: desenvolvimento, design e muita tecnologia()

  • Muito bom o artigo. Não sabia destes detalhes da ListView.

  • Rafael Marcelino

    Boa noite.
    Obrigado pela ajuda. Sou aluno do Alura.

    Uma dúvida. Na classe ViewHolder, os parâmetros não deveriam ser public final?

Próximo ArtigoLinux: compactando e descompactando arquivos com o zip