Nella guida allo sviluppo di GUI per Android, sono stati discussi diversi elementi grafici, come le Toolbar ed i Floating Action Button (FAB), che compongono ormai da anni le interfacce delle più moderne applicazioni Android.
Tra tutti, il componente fondamentale della quasi totalità delle applicazioni esistenti è il RecyclerView
, introdotto con un esempio pratico in questa lezione.
Questo componente ha offerto fin dalle sue prime implementazioni (risalenti al 2014, con il rilascio di Android Lollipop) un nuovo approccio nella risoluzione di un problema comune: la creazione di liste per la visualizzazione di dati ottenuti da un servizio remoto o da un database locale.
In questo approfondimento, vedremo più nel dettaglio i suoi componenti per capirne a pieno il funzionamento.
Perché RecyclerView
È ormai un'usanza comune presentare all'utente una lista di elementi contenente un sottoinsieme di informazioni che la caratterizzano (come foto, un titolo e un sottotitolo) per aiutarlo nella scelta di quale dettaglio consultare. Dal punto di vista dello sviluppo, la realizzazione di questi elementi si riporta alla definizione di un insieme di View
da popolare con le informazioni da mostrare nella viewport, ossia la porzione di schermo visibile dall'utente.
Nonostante venga visualizzato solo un sottoinsieme di elementi sullo schermo, in realtà l'applicazione deve creare il layout anche per tutti gli elementi che non sono visibili nella schermata, senza contare che ogni elemento visualizzato deve essere salvato in memoria per poterlo mostrare in un secondo momento all'utente senza doverlo ricreare.
Questo meccanismo è trascurabile nel caso in cui si debba lavorare con liste di piccole dimensioni. In caso contrario, però, diventa impraticabile utilizzare questo approccio. Purtroppo, il caricamento di un grande insieme di elementi in una sola volta porta ad una rapida saturazione delle memoria, con impatti spiacevoli sulla user experience e sulle prestazioni dell'applicazione.
È qui che viene in aiuto il componente RecyclerView
.
Anziché creare tutti gli elementi della lista durante lo scroll, esso mantiene in una coda, chiamata recycler bin (cestino per il riciclaggio), alcuni degli elementi precedentemente visualizzati per riutilizzarli in un secondo momento. Infatti, durante lo scorrimento della lista, il RecyclerView
recupererà dalla coda un elemento e lo popolerà con le nuove informazioni da mostrare all'utente.
Proprio questo meccanismo di riciclo e i vantaggi offerti (riportati nella tabella seguente) hanno fatto sì che il RecyclerView
si affermasse in questi anni come soluzione principe nella creazione delle liste, sostituendo completamente il suo predecessore: la ListView.
Pro | Riciclo delle viste attraverso l'utilizzo del pattern ViewHolder |
---|---|
Supporto di liste orizzontali, verticali, grid classiche e sfalsate (staggered grid) | |
Supporto per lo scroll orizzontale e verticale (non disponibile con il ListView ) |
|
Miglioramenti in termini di performance e di occupazione della memoria in quanto non è necessario creare il layout da popolare con le informazioni del data source per via del riutilizzo | |
Integrazione di animazioni per aggiungere, aggiornare e rimuovere oggetti | |
Contro | Aumento della complessità |
Mancanza di un metodo nativo per intercettare il click su un elemento della lista |
Componenti
Uno degli aspetti più interessanti del RecyclerView
è proprio la sua modularità, che lo rende facilmente modificabile e riutilizzabile in diversi punti dell'applicazione.
Nella seguente figura, è riportato un semplice schema di comunicazione tra i componenti coinvolti nella creazione di una lista tramite il RecyclerView
.
Vediamo nel dettaglio ciascuno di tali componenti.
Componente | Tipologia | Descrizione |
---|---|---|
Adapter | RecyclerView.Adapter |
È responsabile di estrarre i dati dal Data Source e di usare questi dati per creare e popolare i ViewHolder . Quest’ultimi saranno poi inviati al Layout Manager del RecyclerView.Adapter . |
ViewHolder | RecyclerView.ViewHolder |
È la chiave di volta tra il RecyclerView e l'Adapter e permette la riduzione nel numero di view da creare. Questo oggetto infatti fornisce il layout da popolare con i dati presenti nel DataSource e viene riutilizzato dal RecyclerView per ridurre il numero di layout da creare per popolare la lista. |
Layout Manager | RecyclerView.LayoutManager |
È responsabile della creazione e del posizionamento delle view all'interno del RecyclerView . Esistono diverse tipologie di LayoutManager come il LinearLayoutManager utilizzato per creare liste orizzontali o verticali. |
DataSource | List |
È l'insieme di dati utilizzato per popolare la lista tramite l'Adapter |
Meccanismo di riciclo: esempio pratico
Creiamo un nuovo progetto Android, come illustrato in questa lezione, e chiamiamolo RecyclerViewHMTLit.
Nell'app gradle aggiungiamo alcune librerie, che ci permetteranno di gestire:
- il
RecyclerView
; - il caricamento delle immagini tramite la libreria Picasso, di cui si è parlato in questa lezione;
- il binding tra layout XML e componenti con ButterKnife, introdotto invece in questa lezione.
dependencies {
// ...
implementation 'com.android.support:design:27.1.1'
implementation 'com.android.support:recyclerview-v7:27.1.1'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
Dopo aver configurato correttamente tutte le librerie a nostra disposizione, siamo pronti per creare la nostra lista con RecyclerView
e a comprenderne il meccanismo di riciclo dei ViewHolder
con un semplice esempio basato sul colore del nostro ViewHolder
.
Per fare ciò i passi da compiere sono i seguenti:
- creazione di un asset di colori basati su un gradiente e di una classe che recuperi il colore in base al contesto e al
ViewHolder
creato; - modifica del file activity_main.xml per aggiungere il
RecyclerView
e creazione di un apposito layout per ilViewHolder
; - implementazione dell'
Adapter
e delViewHolder
; - definizione dell'interfaccia per il click sull'oggetto;
- implementazione del
RecyclerView
nellaMainActivity
.
Definizione dei colori di background
All'interno del nostro package principale definiamo un nuovo package di nome utils e creiamo la classe ColorUtils
definita qui.
Questa classe definisce al suo interno il metodo statico seguente:
public static int getViewHolderBackgroundColorFromInstance(Context context, int instanceNum){..}
Esso, dato il contesto dell'applicazione e un intero che rappresenta l'ordine in cui il ViewHolder
è stato creato, ritorna un intero che rappresenta il colore da utilizzare come background.
Nonostante la sua semplicità, questo metodo è fondamentale per capire come i ViewHolder
vengano riciclati all'interno di un RecyclerView
.
Non resta che definire l'asset di colori. In questo caso, definiamo all'interno del file colors.xml (consultabile a questo link) un gradiente di colori che va dal bianco all'arancione passando per il giallo.
Modifica e creazione dei layout
Apriamo il layout della MainActivity
, activity_main.xml, e sostituiamo la TextView
di default con il componente RecyclerView
come segue.
...
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_colored"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
...
Creiamo quindi un nuovo layout, che verrà utilizzato dal ViewHolder
e popolato a run-time dall'Adapter
. Chiamiamo questo layout list_item e definiamo al suo interno un FrameLayout
contenente:
- una
ImageView
che mostrerà il logo di HTML.it - una
TextView
che mostrerà un numero corrispondente alla reale posizione dell'elemento nella lista; - una
TextView
contenente l'effettivo indice delViewHolder
che ci permetterà di capire come vengono ri-utilizzati iViewHolder
creati.
Per completezza, si riporta al seguente link l'implementazione del layout list_item.
Implementazione dell'Adapter e del ViewHolder
Creiamo ora un nuovo package che conterrà al suo interno l'Adapter
ed il relativo ViewHolder
. Chiamiamo il nuovo package Adapter
, ed al suo interno creiamo una nuova classe ItemAdapter
.
Per poter implementare appieno la classe ItemAdapter
è necessario creare prima la classe ItemViewHolder
come inner class.
l'implementazione della classe ItemViewHolder
è alquanto semplice. Tramite ButterKnife facciamo il binding tra gli elementi del layout list_item creato in precedenza e i relativi oggetti View
che li caratterizzano in Java. Successivamente, definiamo il costruttore della classe in cui effettuiamo il binding tramite ButterKnife e implementiamo il metodo bind
che ha il compito di impostare il testo rappresentante la posizione dell'elemento corrente.
public class ItemAdapter {
// ...
class ItemViewHolder extends RecyclerView.ViewHolder{
@BindView(R.id.tv_item_number)
TextView mListItemNumberTV;
@BindView(R.id.tv_view_holder_index)
TextView mVHIndexTV;
@BindView(R.id.iv_logo)
ImageView mIVLogp;
public ItemViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
void bind(int listIndex) {
mListItemNumberTV.setText(String.valueOf(listIndex));
}
}
}
Ora possiamo finalmente estendere la classe RecyclerView.Adapter
come segue.
public class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ItemViewHolder> {
private static int viewHolderCount;
private int mNumberItems;
private Context parentContex;
public ItemAdapter(int numberOfItems) {
mNumberItems = numberOfItems;
viewHolderCount = 0;
}
//...
}
In particolare, abbiamo definito tre attributi che corrispondono al numero di ViewHolder
creati, al numero di elementi della lista totali e al Context
del parent, e abbiamo definito il costruttore della classe.
Per mostrare il meccanismo di riciclo, però, dobbiamo implementare i metodi onCreateViewHolder
e onBindViewHolder
propri della classe RecyclerView.Adapter
.
In particolare, nel metodo onCreateViewHolder
andiamo ad abilitare la creazione di un nuovo ItemViewHolder
, che come parametro prende in ingresso un oggetto di tipo View
, ossia il layout list_item. Creato l'oggetto, impostiamo il colore di background invocando il metodo getViewHolderBackgroundColorFromInstance
della classe ColorUtils
sulla base del valore corrente del viewHolderCount
. Infine, impostiamo l'informazione su quale ViewHolder
stiamo visualizzando e incrementiamo di un’unità la proprietà viewHolderCount
.
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
parentContex = parent.getContext();
int layoutIdForListItem = R.layout.list_item;
LayoutInflater inflater = LayoutInflater.from(parentContex);
View view = inflater.inflate(layoutIdForListItem, parent, false);
ItemViewHolder holder = new ItemViewHolder(view);
int backgroundColorForViewHolder = ColorUtils
.getViewHolderBackgroundColorFromInstance(parentContex, viewHolderCount);
holder.mVHIndexTV.setText("ViewHolder index: " + viewHolderCount);
holder.itemView.setBackgroundColor(backgroundColorForViewHolder);
viewHolderCount++;
return holder;
}
Nel metodo onBindViewHolder
, invece, andiamo a compiere solo due semplici operazioni:
- l'invocazione del metodo
bind
della classeItemViewHolder
per impostare la posizione corrente dell'elemento; - il caricamento dell'immagine del logo di HTML.it dalla cartella drawable.
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
holder.bind(position);
Picasso.get()
.load(R.drawable.logo_open)
.placeholder(R.mipmap.ic_launcher)
.error(R.mipmap.ic_launcher)
.into(holder.mIVLogp);
}
Modifica della MainActivity
Spostiamoci adesso sulla MainActivity
per implementare la RecyclerView
e il metodo di reset della lista.
Definiamo tre variabili per rappresentare il numero di elementi nella lista, il RecyclerView
e l'Adapter
per quest’ultimo. Inoltre, aggiungiamo la notazione @BindView
per il RecyclerView
in modo da mapparlo successivamente nel metodo onCreate
.
private static final int NUM_LIST_ITEMS = 100;
private ItemAdapter mAdapter;
@BindView(R.id.rv_colored)
RecyclerView mList;
Nel metodo onCreate
, eseguiamo i seguenti passi:
- effettuiamo il binding tramite
ButterKnife
; - creiamo un nuovo
LayoutManager
di tipoLinearLayoutManager
e lo associamo alRecyclerView
; - creiamo un nuovo
adapter
di tipoItemAdapter
a cui passiamo il numero di elementi da creare e lo associamo alRecyclerView
.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
mList.setLayoutManager(layoutManager);
mList.setHasFixedSize(true);
mAdapter = new ItemAdapter(NUM_LIST_ITEMS);
mList.setAdapter(mAdapter);
}
Gestione del clic sull'oggetto
Il RecyclerView
differentemente dal ListView
non offre un metodo nativo per intercettare il click su un elemento della lista. Per aggiungere tale comportamento basta compiere pochi semplici passi. Vediamoli insieme.
Sempre all'interno della classe ItemAdapter
, creiamo l'interfaccia ItemClickListener
che riceve il messaggio di clic e definiamo una variabile statica per il listener
:
final private ItemClickListener mOnClickListener;
public interface ItemClickListener {
void onListItemClick(int clickedItemIndex);
}
Modifichiamo il costruttore della classe ItemAdapter
per passare il listener e salvarlo nella variabile mOnClickListener
:
public ItemAdapter(int numberOfItems, ItemClickListener listener) {
// . . .
mOnClickListener = listener;
}
Implementiamo la classe ItemViewHolder
con l'interfaccia View.OnClickListener
per implementare il metodo onClick
che verrà invocato ogni qualvolta l'utente cliccherà su un elemento della lista.
class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
//...
public ItemViewHolder(View itemView) {
// ...
itemView.setOnClickListener(this);
}
@Override
public void onClick(View view) {
int clickedPosition = getAdapterPosition();
mOnClickListener.onListItemClick(clickedPosition);
}
}
Infine, modifichiamo come segue la MainActivity
per implementare l'interfaccia ItemClickListener
e modificare la creazione dell'ItemAdapter
.
public class MainActivity extends AppCompatActivity implements ItemAdapter.ItemClickListener {
// ...
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
mAdapter = new ItemAdapter(NUM_LIST_ITEMS, this);
mNumbersList.setAdapter(mAdapter);
}
@Override
public void onListItemClick(int clickedItemIndex) {
if (mToast != null) {
mToast.cancel();
}
String toastMessage = "Item #" + clickedItemIndex + " clicked.";
mToast = Toast.makeText(this, toastMessage, Toast.LENGTH_LONG);
mToast.show();
}
}
Risultato finale
Eseguiamo infine la nostra applicazione per vedere effettivamente il risultato del lavoro fatto.
Come è possibile vedere, l'applicazione ci mostra una lista di elementi composti da:
- un numero che identifica la posizione dell'elemento che stiamo visualizzando;
- il logo di HMTL.it caricato attraverso la libreria Picasso;
- l'indice del
ViewHolder
che è stato creato e visualizzato.
In questo caso, scorrendo la lista si può notare che il numero relativo della posizione aumenta e il colore del background cambia finché non si raggiunge il ViewHolder
avente come indice il valore 12.
Dall'elemento 13 della lista in poi, il RecyclerView
mette in moto il meccanismo di riciclo dei ViewHolder
, recuperandoli dal recycle bin per popolarli con le nuove informazioni fornite dall'Adapter
.
Si potrebbe (erroneamente) pensare che il RecyclerView
ritulizzi in modo ordinato i 13 ViewHolder
creati, ma in realtà non è così.
Come mostrato nella Figura 2, scorrendo velocemente la lista e poi osservandola, si può notare l'efficienza del meccanismo di riciclo che recupera dal suo recycler bin i ViewHolder
in quel momento disponibili e li mostra all'utente finale senza perdite in termini di prestazioni.
Questo ovviamente si traduce in una lista in cui le informazioni restano sempre le stesse, ma ciò che cambia è l'ordine in cui i ViewHolder sono utilizzati dal RecyclerView
.
Infine, cliccando su uno degli elementi comparirà un Toast
che ci comunicherà quale elemento è stato cliccato.
Questo meccanismo ha fatto sì che RecyclerView
diventasse lo strumento principale per la realizzazione di liste, nonostante la sua implementazione non sia così rapida come quella del ListView
di cui si è parlato in precedenza qui.
Il codice di questa lezione è disponibile su GitHub.