Personalizando uma ListView no Android

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… Eu queria que cada item tivesse um layout e um design da minha preferência como, por exemplo, esse item que eu criei:

item-personzalido-lista

<?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? 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 {

}

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

}

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();
}

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);
}

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.

Ó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;
}

Ó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

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!

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

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