Olá povo,
Devia ter escrito esse post a muito tempo, mas finalmente saiu :) Desde a primeira versão do Android em 2008 até o começo de 2014, para criarmos telas de listagem utilizávamos a classe ListView juntamente com alguma subclasse de BaseAdapter (ArrayAdapter ou CursorAdapter por exemplo) e esse conjunto funcionava muito bem. Mas conforme o tempo foi passando, ele foi apresentando algumas "limitações" que atrapalhavam sua conformidade aos requisitos/padrões de UI/UX atuais. Nada que fosse impossível de ser implementado, mas que eram mais trabalhosos de serem feitos, tais como:
- Performance na atualização de itens: a ListView está ligada ao Adapter, que possui uma lista de objetos. Se inserirmos um objeto nessa lista, temos que invocar o método notifyDatasetChanged() que fará com que toda a lista seja refeita/redesenhada. Com a RecyclerView podemos atualizar só um item da lista (inserindo/atualizando/excluindo) ou um intervalo específico.
- Layouts diferenciados para cada situação: com a RecyclerView podemos configurar gerenciadores de layouts, indicando por exemplo, que a lista terá uma única coluna quando o aparelho estiver em portrait e duas quando estiver em landscape. Ou ainda dizer que o primeiro item da lista será diferente dos demais.
- Animações e gestos: Com a RecyclerView, à medida que os itens vão sendo adicionados ou removidos, uma animação é realizada dando um feedback visual para o usuário do que aconteceu. A utilização de gestos também ficou bastante simples. Ações como o swipe sobre um item da lista é algo trivial de ser feito.
- Scroll em ambos os sentidos: a RecyclerView permite o scroll em na horizontal e na vertical, o que não era possível nativamente na ListView.
- Baixa curva de aprendizagem: o conceito utilizado pela RecyclerView é muito parecido com o que temos na ListView. Então quem já a conhece não terá muitos problemas.
Estes são os mais importantes para mim. Mas já pode te convencer a mudar para a RecyclerView. Se é que não já mudou? Você já mudou, certo? ;)
Pondo em prática
Veremos um exemplo simples de uma listagem de mensagens, mas que obviamente pode ser ajustado para qualquer outro propósito. Adicione as seguintes dependências no
build.gradle do projeto.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.1.1'
compile 'com.android.support:design:23.1.1'
compile 'com.android.support:cardview-v7:23.1.1'
compile 'com.jakewharton:butterknife:7.0.1'
}
A RecyclerView está biblioteca
appcompat. Já a biblioteca de
design foi adicionada aqui para podermos utilizar o
FloatActionButton. Cada item da lista será um
CardView, então adicionamos a dependência. E por fim, utilizamos a
ButterKnife que falei nesse post
aqui.
Vamos agora para a implementação começando pela classe básica.
public class Mensagem {
public String titulo;
public String texto;
public Mensagem(String titulo, String texto) {
this.titulo = titulo;
this.texto = texto;
}
}
Nada a comentar sobre essa classe, então vamos adicionar o arquivo de layout
item_mensagem.xml que representará cada item da lista.
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="4dp"
android:foreground="?android:attr/selectableItemBackground"
app:cardCornerRadius="5dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/txtTitulo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"/>
<TextView
android:id="@+id/txtMensagem"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"/>
</LinearLayout>
</android.support.v7.widget.CardView>
Um ponto interessante a ressaltar nesse layout é a propriedade
foreground. A RecyclerView, por padrão, não fornece o feedback de toque como faz a ListView, pois ela parte do pressuposto que seus itens não são clicáveis, ao contrário da ListView que mesmo que não haja tratamento do evento de clique, um feedback visual é dado ao usuário.
O arquivo de layout da activity (
activity_main.xml) é exibido a seguir:
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/titulo"
android:id="@+id/edtTitulo"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/texto"
android:id="@+id/edtTexto"/>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recyclerView"/>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:id="@+id/fab"
android:src="@drawable/ic_add_white_24dp"/>
</FrameLayout>
O único detalhe a comentar sobre esse layout é a imagem do ícone do botão. Ela pode ser encontrada no
site de ícones do Material Design, que fornece mais de 800 ícones gratuitos que você pode utilizar no seu aplicativo.
Vamos agora implementar o adapter a ser utilizado por nossa RecyclerView.
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
public class MensagemAdapter extends
RecyclerView.Adapter<MensagemAdapter.VH> {
List<Mensagem> mMensagens;
AoClicarNaMensagem mListener;
public MensagemAdapter(List<Mensagem> mensagens,
AoClicarNaMensagem listener) {
mMensagens = mensagens;
mListener = listener;
}
@Override
public VH onCreateViewHolder(ViewGroup parent,
int viewType) {
View v= LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_mensagem,
parent, false);
final VH vh = new VH(v);
vh.itemView.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) {
int pos = vh.getAdapterPosition();
Mensagem mensagem = mMensagens.get(pos);
mListener.mensagemClicada(mensagem);
}
}
});
return vh;
}
@Override
public void onBindViewHolder(VH holder, int pos) {
Mensagem msg = mMensagens.get(pos);
holder.textTitulo.setText(msg.titulo);
holder.textTexto.setText(msg.texto);
}
@Override
public int getItemCount() {
return mMensagens != null ?
mMensagens.size() : 0;
}
public static class VH extends
RecyclerView.ViewHolder {
@Bind(R.id.txtTitulo)
public TextView textTitulo;
@Bind(R.id.txtMensagem)
public TextView textTexto;
public MensagemViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
public interface AoClicarNaMensagem {
void mensagemClicada(Mensagem mensagem);
}
}
Esssa sem sombra de dúvida é a classe mais importante desse exemplo. Então vamos aos detalhes sobre ela:
- Para definir um adapter para a RecyclerView devemos criar uma subclasse de RecyclerView.Adapter, e como podemos observar, ela é "tipada". Ou seja, ela necessita de um tipo que deve ser uma subclasse de RecycleView.ViewHolder. No nosso exemplo, essa classe é chamada simplesmente de VH e está declarada dentro da própria classe MensagemAdapter.
- Para criar uma instância do nosso adapter, além da lista de objetos, podemos passar um objeto que tratará o evento de clique do item da lista. Diferentemente do que é feito na ListView, onde temos o OnItemClickListener, o evento de clique em um item da lista é definido no próprio item, ou seja, no adapter.
- No nosso caso, quem estiver interessado em ouvir o evento de clique no item da lista, deverá passar como parâmetro para o adapter, um objeto que implemente a interface AoClicarNaMensagem. Na prática, a activity passará esse objeto para o adapter.
- Obrigatoriamente devemos implementar 3 métodos:
- O método onCreateViewHolder criará a instância do ViewHolder baseado no arquivo de layout que representa cada item (no nosso caso item_mensagem.xml). Esse método já implementa, por padrão, a abordagem de um adapter eficiente que expliquei nesse post, e esse é o momento ideal para definirmos o evento de clique do item da lista. Percebam que por meio do atributo itemView (que já é da classe ViewHolder) definimos o evento de clique. E usando o método getAdapterPosition() podemos obter o índice da posição da lista que foi clicada.
- No onBindViewHolder() é onde preenchemos cada View do ViewHolder. Essa abordagem de create/bind já era usada no CursorAdapter como falei nesse post.
- O getItemCount() é quantidade de itens que sua lista exibirá, como já é de costume em qualquer adapter.
- Percebam que estamos utilizando o ButterKnife no ViewHolder, mas fique a vontade em usar o bom e velho findViewById() :)
Vamos utilizar esse adapter na Activity.
public class MainActivity extends AppCompatActivity {
@Bind(R.id.edtTitulo)
EditText mEdtTitulo;
@Bind(R.id.edtTexto)
EditText mEdtMensagem;
@Bind(R.id.recyclerView)
RecyclerView mRecyclerView;
List<Mensagem> mMensagens;
MensagemAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mMensagens = new ArrayList<>();
mAdapter = new MensagemAdapter(
mMensagens, mListener);
mRecyclerView.setAdapter(mAdapter);
GridLayoutManager layoutManager =
new GridLayoutManager(this, 2);
layoutManager.setSpanSizeLookup(
new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int pos) {
return pos == 0 ? 2 : 1;
}
});
mRecyclerView.setLayoutManager(layoutManager);
}
@OnClick(R.id.fab)
public void onClick(View view) {
Mensagem mensagem = new Mensagem(
mEdtTitulo.getText().toString(),
mEdtMensagem.getText().toString());
mMensagens.add(mensagem);
mAdapter.notifyItemInserted(
mMensagens.size() - 1);
mEdtTitulo.getText().clear();
mEdtMensagem.getText().clear();
}
private MensagemAdapter.AoClicarNaMensagem mListener=
new MensagemAdapter.AoClicarNaMensagem() {
@Override
public void mensagemClicada(Mensagem msg) {
String s = String.format(
"%s %s", msg.titulo, msg.texto);
Toast.makeText(MainActivity.this,
s, Toast.LENGTH_SHORT).show();
}
};
}
Vamos agora as explicações relevantes:
- Estamos utilizando o gerenciador de layout GridLayoutManager que nos permite dividir nossa lista em colunas.
- No nosso caso, estamos dizendo que teremos duas colunas. O método setSpanSizeLookup() indica quais itens da linha vão ocupar mais de uma coluna. Para esse exemplo, estipulei que apenas o primeiro item da lista vai ocupar duas colunas, os demais ocuparão uma (das duas colunas) da lista.
- Existem ainda os gerenciadores de layout LinearLayoutManager e StaggeredGridLayoutManager. O primeiro organiza os itens da lista de forma linear enquanto o segundo organiza os itens em forma de grid, mas suporta itens de tamanhos completamente diferentes e os exibe como se fosse um "mosaico".
- E você pode criar seus próprios gerenciadores de layout.
- Percebam que no evento de clique, ao inserirmos um registo, utilizamos o método notifyItemInserted(). E como sempre adicionamos o item ao final da lista, passamos o tamanho da lista menos um.
- Existem também os métodos notifyItemRemoved(), notifyItemChanged(), notifyItemMoved() para quando um item da lista foi respectivamente removido, alterado ou movido de uma posição para a outra da lista.
- Podemos utilizar os métodos notifyItemRangeInserted(), notifyItemRangeRemoved() e notifyItemRangeChanged(). Que indicam que um intervalo de itens da lista foi inserido, removido ou alterado.
- Obviamente existe o notifyDatasetChanged(), que possui o mesmo comportamento da ListView.
Ao executar o exemplo e adicionar alguns itens, você terá um resultado similar ao da figura abaixo.
O aplicativo deve estar adicionando os itens na lista. Mas e para remover? Que tal usar o gesto de swipe? ;) Com a RecyclerView isso ficou bem simples. Vejamos o código a seguir.
private void configuraSwipe() {
ItemTouchHelper.SimpleCallback swipe =
new ItemTouchHelper.SimpleCallback(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(
RecyclerView.ViewHolder viewHolder,
int swipeDir) {
final int position =
viewHolder.getAdapterPosition();
mMensagens.remove(position);
mAdapter.notifyItemRemoved(position);
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(swipe);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
}
Com a classe
ItemTouchHelper estamos configurando o gesto de swipe para esquerda e para direita. E no método
onSwiped() removemos o item da lista e notificamos o adapter. Perceba que com essa classe também podemos fazer o evento de mover itens na lista.
Para concluir, chame esse método no
onCreate() da activity.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
configuraSwipe();
}
Execute a aplicação novamente e insira alguns itens. Em seguida, tente exclui-los utilizando o gesto de swipe. Perceba que após um item ser excluído, os demais itens são reorganizados com uma animação.
Conclusão
Se você ainda não usa a RecylerView, você está perdendo diversos recursos bacanas que estão sendo adicionados a esse componente. Entretanto ele ainda tem algumas melhorias a serem feitas e o próprio pessoal do Google admitiu isso para mim no Android Dev Summit.
Coisas simples como a
empty view já poderiam estar implementadas (nada demais, eu convivo com isso sem problemas). Definir um
divisor entre as linhas da RecyclerView é um absurdo. Isso já deveria estar abstraído no componente. Outra coisa que eu sentia falta era um
CursorAdapter para a RecyclerView. Mas depois de ver a implementação interna dele, achei melhor fazer o meu mesmo :)
No final das contas, sem sombra de dúvidas o RecyclerView é bem mais flexível que a ListView e o seu resultado final vale muito a pena.
Qualquer dúvida, deixem seus comentários.
4br4ç05,
nglauber
P.S.: Meus agradecimentos a
André Mion que me ajudou a discutir os tópicos desse post.