Personalizando uma ListView no Android

(Última atualização em: 10 de julho 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

}

Observe que não foi apresentada a implementação do método todosOsCursos() justamente para ser mais objetivo na implementação do componente ListView. Compreenda esse método como um acesso aos dados qualquer, seja uma lista estática, banco de dados ou qualquer meio que devolva uma lista de cursos.

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.

Código fonte

Caso tiver dúvidas ou simplesmente quiser consultar o código fonte do projeto utilizado como exemplo, fique à vontade de dar uma olhada no github. Lembrando que para essa estapa do projeto eu fiz uso da branch lista-personalizada.

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? O legal é que depois de aprender Android, você pode inclusive partir pro Swift e programar para o iOS. O mesmo vale para os nossos cursos presenciais na Caelum sobre Android, que são extremamente práticos e te ensinam do básico ao avançado.

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 🙂

  • leandro

    man poderia disponibilizar o arquivo???

    • Alex Felipe

      Oi Leadro, blz? O arquivo que você diz seria o projeto com a implementação realizada no post? Ou apenas algum arquivo específico que foi explicado?

      Abraços.

  • Samuel Hcp

    De onde surgiu o todosOsCursos(); ? Poderia me dar uma explicação de como você estruturou ele

    • Alex Felipe

      Oi Samuel, blz? Entenda esse método como uma implementação que devolve uma lista de cursos qualquer, seja estática ou via banco de dados. Editei o post e agora ele explica o motivo da falta de implementação.

      Abraços.

  • Allan Santos

    muito bom o artigo parabéns ajudou bastante!

    • Alex Felipe

      Obrigado pelo feedback Allan 🙂

  • Luis Paulo

    ola amigo…seria possivel colocar uma imagem armazenada em um banco de dados? mysql no caso…

    • Alex Felipe

      Olá Luis, tudo bem? Eu nunca fiz uma implementação que buscasse uma imagem diretamente de um banco de dados por meio do Android, no caso de um blob… Uma alternativa que a galera costuma fazer é carregar diretamente a imagem a partir de uma URL. Para isso existem bibliotecas que ajudam a fazer isso, como é o caso do Glide.

      Uma abordagem comum para isso é armazenar todas as imagens em um serviço com o S3 da Amazon, então, você apenas carrega as imagens com a URL.

      • Luis Paulo

        penso algo assim..mas tenho medo que fique lento..quando o cliente for usar 3g

        • Alex Felipe

          Então, de qualquer maneira o cliente vai ter que buscar a imagem via alguma requisição, seja para o seu servidor ou qualquer outro. Em outras palavras, é um caminho inevitável…

          Uma das coisas que essas ferramentas como o Glide permite é o cache dos recursos carregados, ou seja, você baixa apenas uma única vez e o conteúdo fica em cache. Dessa forma, evita a questão de todas as vezes ficar tendo que pedir a imagem.

          []s

          • Luis Paulo

            valeu…vou fazer uns testes… []

  • Vinnycius Amâncio

    Olá, seria possível compartilhar o código????

    • Alex Felipe

      Oi Vinnycius, tudo bem?

      Infelizmente, quando fiz esse exemplo acabei não salvando no Github, portanto, eu não teria o projeto pronto em si. Entretanto, adicionei no backlog da equipe de conteúdo para adicionar os códigos fontes de posts que usarem um projeto como é o caso desse. Assim que tiver disponível eu compartilho com você, e também, adiciono no texto.

      []s

  • Isaque Coelho

    Olá, eu não entendi o final do exemplo, na parte em que se popula a ListView, porque ali você está fazendo a referência: ” ListView listaDeCursos = (ListView) findViewById(R.id.lista); “, só que no xml: ” activity_lista_de_cursos ” apresentado lá em cima não existe nenhum componente ListView, e nem ao menos algum componente com o ” android:id=lista “, então como você está populando os componentes?

    • Alex Felipe

      Oi Isaque, tudo bem? Acabo abordando essa parte do ListView neste post aqui. Caso tiver alguma dúvida me avisa. Aproveitando, estou pra deixar todo o código feito nos posts no github.

      []s

      • Isaque Coelho

        Ah sim, eu vi o outro post, e deu certo, daí vim para ver como é a implementação do ListView personalizado, por isso estranhei, no outro post existe o componente ListView com id “lista” no arquivo de layout, só que nesse não há referencia desse componente no seu arquivo de layout, só há o ImageView e as TextViews, e ainda assim você está fazendo na sua Activity a listaDeCursos referenciar a ListView lista, que foi criada apenas no exemplo de ListView Simples, e depois você popula essa ListView usando o ” listaDeCursos.setAdapter(adapter); “. Obrigado por vir responder, eu realmente quero entender como é feito essa ListView personalizada e falta só essa parte de popular a ListView. Poderia testar o código e disponibilizar o link do github?

        • Alex Felipe

          Oi Isaque, tudo bem? Acabei de adicionar o projeto no github e no post! Da uma olhada e veja se agora fica mais claro 🙂

          []s

          • Isaque Coelho

            Ola, eu olhei o código no github, obrigado por disponibilizar, mas ainda tenho dúvidas, lá você tem um código que é um exemplo de ListView simples, sua activity esta usando ArrayAdapter e não tem uma classe BaseAdapter como mostrado no exemplo aqui deste seu post, poderia disponibilizar o exemplo de ListView Personalizado também e colocar o link aqui no post?

          • Alex Felipe

            Oi Isaque, desculpa, tinha esquecido de fazer o commit com as classes ajustadas no meu código, obrigado por avisar. Veja se agora está de acordo. Acabei de fazer o push

Próximo Artigo100 SQLs erradas ao ano e um único caractere