Personalizando uma ListView no Android

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

No post sobre como criar listas com o ListView que eu escrevi anteriormente, vimos como é possível criar uma lista bem básica no Android. O resultado da lista criada foi:

tela-lista-com-cursos

Mas pensando bem, não era exatamente uma lista assim que eu queria… Em outras palavras, seria melhor se cada item tivesse um layout e um design da minha preferência como, por exemplo, esse item que eu criei:

item-personzalido-lista

Código fonte:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:orientation="horizontal">


    <ImageView
        android:id="@+id/lista_curso_personalizada_imagem"
        android:layout_width="100dp"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/lista_curso_personalizada_nome"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Titulo"
            android:textSize="30dp" />

        <TextView
            android:id="@+id/lista_curso_personalizada_descricao"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="descriçao"
            android:textSize="20dp" />

    </LinearLayout>

</LinearLayout>

Mas como será que podemos fazer isso?

Bem… Lembra como fazíamos para adicionar um item na lista? Nós utilizamos a classe ArrayAdapter do Android que era responsável em adaptar itens em uma ListView!

Mas ela é uma implementação já pronta do Android e não conseguimos manipular esse adapter da forma que desejamos…

E agora? Bom, se não podemos modificar a implementação do Android, precisamos implementar a nossa! E como podemos implementar um adapter nosso?

Criando nosso próprio Adapter

Para a nossa felicidade, o Android nos fornece a classe BaseAdapter que permite a criação de um adapter personalizado! Então vamos criar uma nova classe que representará o nosso novo adapter e vamos estender a classe BaseAdapter:

public class AdapterCursosPersonalizado extends BaseAdapter {

}

Implementando métodos necessários do BaseAdapter

Porém, a classe BaseAdapter possui 4 métodos abstratos, ou seja, métodos que a implementação é obrigatória! Precisamos implementar esses métodos:

public class AdapterCursosPersonalizado extends BaseAdapter {

    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        return null;
    }
}

Observe os 3 primeiros métodos: getCount(), getItem(int position), getItemId(int position). Perceba que são métodos relacionados a uma lista! Para implementá-los da maneira correta, precisamos de uma lista dentro do nosso adapter.

Lembra que enviávamos a nossa lista via construtor no ArrayAdapter? Faremos o mesmo no nosso adapter para que possamos implementar esses métodos da maneira esperada:

public class AdapterCursosPersonalizado extends BaseAdapter {

    private final List<Curso> cursos;

    public AdapterCursosPersonalizado(List<Curso> cursos, Activity act) {
        this.cursos = cursos;
    }

    //métodos

}

Informando o total de itens da lista

Agora podemos implementar os nossos métodos! Vamos começar pelo getCount(). O próprio método já diz o que ele faz: conta quantos itens existem na lista. Ou seja, o tamanho da lista.

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

Devolvendo o item da lista pela posição

Agora vamos para o getItem(int position). Veja que ele quer saber um item a partir de uma posição. Isso é fácil! Basta apenas retornamos por meio do método get() mandando a posição:

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

Devolvendo o id do item da lista

Vejamos o próximo: getItemId(int position). Esse método espera saber qual é o id do objeto que está sendo buscado. Porém, se verificarmos a nossa classe que representa um curso:

public class Curso {

    private String nome;
    private String descricao;
    private EstadoAtual estado;

    //métodos

}

Veja que ela não possui um id. Para esse caso nós temos duas alternativas:
– 1) Manter o retorno como 0;
– 2) Adicionar o id ao curso e, pegar o objeto pelo método get() e então usar o getter do id.

Atualmente, não precisamos do id, então, por enquanto, devolveremos 0.

Criando a View para cada item

Ótimo, implementamos os 3 primeiro métodos referente a lista que enviamos, porém ainda falta mais 1 que é o getView():

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     return null;
}

Repare que agora ele retorna uma View, ou seja, esse é o método responsável pela construção de cada item!
O que precisamos para implementá-lo?

Inicialmente, precisamos, de alguma forma, pegar a View que representa o nosso layout personalizado. Afinal é ela que queremos apresentar na nossa lista!

Mas, para pegar uma View, nós precisamos de uma Activity e a nossa classe, além de não ser uma Activity, não possui uma Activity.

E agora? O que faremos? Se dermos uma olhada na forma que fizemos para instanciar o ArrayAdapter anteriormente:

ArrayAdapter<Curso> adapter = new ArrayAdapter<Curso>(this, 
        android.R.layout.simple_list_item_1, cursos);

Veja que estamos passando passando o parâmetro this que representa o objeto da própria Activity que está fazendo a chamada. Precisamos receber também essa Activity via construtor:

public class AdapterCursosPersonalizado extends BaseAdapter {

    private final List<Curso> cursos;
    private final Activity act;

    public AdapterCursosPersonalizado(List<Curso> cursos, Activity act) {
        this.cursos = cursos;
        this.act = act;
    }

    //métodos

}

Agora sim podemos chamar uma View!

Queremos criar uma View, ou seja, ao invés de só buscá-la via o método findViewById(), nós iremos criar a View.

Em outras palavras, inflar uma View! E para isso iremos utilizar o método getLayoutInflater() da Activity que é responsável em inflar uma View:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     act.getLayoutInflater()
     return null;
}

Pegamos o responsável em inflar e chamaremos o método inflate() que criará a View e a retornará para nós:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     View view = act.getLayoutInflater()
          .inflate(R.layout.lista_curso_personalizada, parent, false);
     return null;
}

Perceba que utilizamos o parent que vem como parâmetro do método getView(). Mas o que ele representa?
Como podemos ver, o parent é a própria ViewGroup, ou seja, o layout pai ao qual iremos adicionar a nossa lista, por isso enviamos ele.

Além disso, ainda existe o último parâmetro que recebe um valor booleano, esse parâmetro indica se queremos criar, nesse exato momento a View.

Mas, não fizemos nenhum tipo de alteração como adicionar as informações do curso, por isso mandamos o false. Dessa forma, podemos associar tudo que queremos e só depois ele criará de fato a View 🙂

Certo, pegamos a nossa View e agora precisamos de um curso, certo? Mas qual curso?

Veja que, ainda existe um parâmetro no getView() que é o position, ou seja, é justamente nessa posição que devemos pegar o elemento da lista que foi passada, nesse nosso caso, o curso:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     View view = act.getLayoutInflater()
          .inflate(R.layout.lista_curso_personalizada, parent, false);
     Curso curso = cursos.get(position);
     return null;
}

Preenchendo os valores para cada item da lista

Ótimo! Agora já podemos chamar as outras Views e preencher as informações e então, retornar o objeto view:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
     View view = act.getLayoutInflater()
          .inflate(R.layout.lista_curso_personalizada, parent, false);
     Curso curso = cursos.get(position);

     //pegando as referências das Views
     TextView nome = (TextView) 
     view.findViewById(R.id.lista_curso_personalizada_nome);
     TextView descricao = (TextView) 
     view.findViewById(R.id.lista_curso_personalizada_descricao);
     ImageView imagem = (ImageView) 
     view.findViewById(R.id.lista_curso_personalizada_imagem);

     //populando as Views
     nome.setText(curso.getNome());
     descricao.setText(curso.getDescricao());
     imagem.setImageResource(R.drawable.java);

     return view;
}

O nosso próprio adapter está implementado:

public class AdapterCursosPersonalizado extends BaseAdapter {

    private final List<Curso> cursos;
    private final Activity act;

    public AdapterCursosPersonalizado(List<Curso> cursos, Activity act) {
        this.cursos = cursos;
        this.act = act;
    }

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

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

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = act.getLayoutInflater()
        .inflate(R.layout.lista_curso_personalizada, parent, false);


        Curso curso = cursos.get(position);

        TextView nome = (TextView) 
        view.findViewById(R.id.lista_curso_personalizada_nome);
        TextView descricao = (TextView) 
        view.findViewById(R.id.lista_curso_personalizada_descricao);
        ImageView imagem = (ImageView) 
        view.findViewById(R.id.lista_curso_personalizada_imagem);

        nome.setText(curso.getNome());
        descricao.setText(curso.getDescricao());
        imagem.setImageResource(R.drawable.java);

        return view;
    }
}

Para testarmos a nossa implementação, basta apenas alterar na Activity, que chamava o ArrayAdapter do Android, para chamar o nosso adapter:

public class ListaDeCursosActivity extends AppCompatActivity {

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_lista_de_cursos);

        List<Curso> cursos = todosOsCursos();

        ListView listaDeCursos = (ListView) findViewById(R.id.lista);
        
        //chamada da implementaçao do android: 
        //ArrayAdapter<Curso> adapter = new ArrayAdapter<Curso>(this, 
        //android.R.layout.simple_list_item_1, cursos);
        
        //chamada da nossa implementação
        AdapterCursosPersonalizado adapter = 
             new AdapterCursosPersonalizado(cursos, this);

        listaDeCursos.setAdapter(adapter);

    }

    //métodos

}

Agora se testarmos a nossa app:

tela-lista-personalizada1

Adicionando uma imagem diferente para cada item da lista

A nossa lista mudou, porém ainda há uma coisa bem bizarra: no curso de Java temos a imagem do Java, no de HTML também, e no de Android também! E não era isso que nós queríamos!

Nós queremos que, para cada curso, seja usada uma imagem que identifique-o. Por exemplo: curso de Java imagem de Java, curso de HTML, imagem de HTML e assim por diante. O que será que erramos?

Vejamos como está sendo inserida a imagem no getView() do nosso adapter:

 
@Override
public View getView(int position, View convertView, ViewGroup parent) {

        //código

        Curso curso = cursos.get(position);

        ImageView imagem = (ImageView) view
            .findViewById(R.id.lista_curso_personalizada_imagem);

        imagem.setImageResource(R.drawable.java);

        return view;
}

Observe que estamos setando a mesma imagem para todos os elementos da lista! Precisamos de alguma informação do curso para sabermos a que ele se refere!

Atualmente, não temos nenhum tipo de informação para categorizar os nossos cursos, então que tal criarmos um enum para isso?

public enum Categoria {

    JAVA, HTML, ANDROID;
    
}

E agora adicionamos um enum para a nossa classe curso:

public class Curso {

    private long id;
    private String nome;
    private String descricao;
    private EstadoAtual estado;
    private Categoria categoria;

    //métodos

}

Muito bom! Agora basta verificarmos a qual categoria o curso refere-se e então settamos a imagem apropriada:

 
@Override
public View getView(int position, View convertView, ViewGroup parent) {

        //código

        Curso curso = cursos.get(position);

        ImageView imagem = (ImageView) view
                .findViewById(R.id.lista_curso_personalizada_imagem);

        Categoria categoria = curso.getCategoria();

        if (categoria.equals(Categoria.JAVA)) {
            imagem.setImageResource(R.drawable.java);
        } else if (categoria.equals(Categoria.ANDROID)) {
            imagem.setImageResource(R.drawable.android);
        } else if (categoria.equals(Categoria.HTML)) {
            imagem.setImageResource(R.drawable.html);
        }

        return view;
}

Essa solução com esse tanto de if e else funciona, porém não é uma boa prática! Nesse post eu detalho um dos grandes problemas que temos com esse tipo de solução e como podemos resolver de uma maneira mais elegante.

Adicionamos as nossas condições para setar as imagens, então agora vamos testar e ver o resultado:

tela-lista-personalizada2

Excelente! A nossa lista personalizada foi criada conforme o esperado!

Aprendendo um pouco mais

Embora tenhamos aprendido a implementar e personalizar uma ListView ainda existe mais um detalhe importante durante a implementação de um Adapter que é justamente o padrão ViewHolder.

Não será explicado sobre esse padrão, entretanto, o Matheus escreveu um excelente post explicando a teoria e como é possível implementar o ViewHolder na ListView.

Resumo

Vimos que, para criar uma lista personalizada, nós precisamos fazer uma implementação nossa estendo da classe BaseAdapter que permite a criação de um adapter personalizado.

Vimos também que temos que especificar tudo que iremos adicionar na lista, como por exemplo, as informações do curso para sua View específica.

Além disso, vimos que podemos declarar condições no método getView() para adicionar conteúdo diferente de acordo com algum critério, como foi o caso da imagem específica para cada curso.

E aí, gostou de criar uma lista própria? Quer aprender mais dicas sobre o Android? Que tal conhecer os cursos para mobile da Alura? Com cursos de Android, Swift e muito mais, para que você possa aprimorar os seus conhecimentos e ingressar sua carreira no mercado mobile.

Content Editor at Alura and Software Developer

  • Pingback: Utilizando o padrão ViewHolder - Blog da Alura: desenvolvimento, design e muita tecnologia()

  • Christiano Dos Santos

    muito bom. eu tomei como base pra aprender e começar a desenvolver um app que vai precisar uma lista personalizada. No entanto, como disse, usar um if…else p/ selecionar a imagem adequada, não é nada bom, ainda mais com lista grande de itens. Eu pensei num modo mais direto: a classe da entidade contém uma propriedade int idImagem; a qual guarda o valor Resource.Drawable.nomeimagem. assim, no adapter, é possíver definir a imagem via: imagem.SetImageResource(itemList[position].IdImagem);

    • Alex Felipe

      Opa Christiano, tudo bem?

      Eu dei uma lida no post e realmente estava faltando uma referência para o meu post que ensina justamente os passos que você realizou! Porém, é mais detalho o que significa esses passos em programação. Recomendo a leitura -> http://blog.alura.com.br/reduzindo-de-n-ifs-para-nenhum-com-strategy-em-java/

      Obrigado pelo comentário e parabéns por chegar a essa conclusão 🙂

      Abraços.

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

  • Jilles Ragonha

    Muito Bom Alex, mas deixa eu perguntar algo,
    queria fazer uma lista, bem parecida com essa da imagem:
    http://prntscr.com/egrhbm

    como faço para criar esse “titulo” com a letra do alfabeto?

    • Alex Felipe

      Opa Jilles, blz? Fazer uma breve explicação não seria uma boa, ou seja, eu precisaria escrever um post para explicar essa parte… Bom, posso te passar um link com um tutorial explicativo mostrando como é possível adicionar essas seções dentro da listView -> http://android.amberfog.com/?p=296

      Abraços.

  • Cara parabéns pelo artigo, muito bem explicado.

    • Alex Felipe

      Que bom que tenha gostado Victor! Obrigado pelo feedback 🙂

Próximo ArtigoJava 9 na prática: Inferência de tipos