L'Adapter è un pattern già presentato nel corso di questa guida. È il meccanismo per impostare agevolmente la visualizzazione di oggetti Java su un layout di un'app Android.
Finora le accoppiate AdapterView-Adapter utilizzate negli esempi sono state ListView-ArrayAdapter o GridView-ArrayAdapter.
Di Adapter ne esistono molti nel framework. La documentazione offre tutti i dettagli in merito ma spesso capita di aver bisogno di creare una visualizzazione personalizzata.
In questi casi, si può realizzare un Adapter in versione custom ed in questa lezione vedremo come. Studiare questa casistica fornisce il programmatore non solo di uno strumento utilissimo, ma anche di un'esperienza formativa molto significativa che permette di osservare il funzionamento di un Adapter “dall'interno”.
Nell'esempio che andremo ad utilizzare, un'Activity mostra un elenco di articoli. Potrebbe essere l'interfaccia di un NewsReader ma qui verrà trattata in maniera simulata. Gli oggetti Java che rappresentano articoli, i cui dati provengono dalle recenti pubblicazioni di HTML.it, sono prodotti da un metodo di supporto interno all'Activity.
Ogni articolo, nel progetto, è rappresentato da un oggetto di classe ArticlesInfo i cui membri rappresentano, rispettivamente, il titolo dell'articolo, la categoria tematica di appartenenza e la data di pubblicazione:
public class ArticleInfo {
private String title;
private String category;
private Date date;
/*
* OMISSIS: la classe possiede tutti i setter e i getter
* per gestire i membri privati
*
*
*/
}
Si noti che per la gestione delle date si è scelto di utilizzare la
classe Date
appartenente ad una gestione "tradizionale"
delle informazioni temporali in Java, ciò come soluzione universalmente valida indipendentemente dal range di API Android che
l'utente desidera supportare. Tuttavia, vale la pena ricordare che a partire dalle API 26 le classi Java di nuova generazione per
la gestione di date e orari sono facilmente integrabili nelle app Android.
Affinché ognuno di questi oggetti, per così dire, si trasformi in una riga della ListView contenuta nell'Activity prepariamo subito un layout nel file res/layout/listactivity_row_article.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/row_background"
android:descendantFocusability="blocksDescendants"
android:padding="10dp">
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_below="@+id/txt_article_datetime"
android:orientation="vertical"
android:id="@+id/ll_text">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/big_textstyle"
android:id="@+id/txt_article_description"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/small_textstyle"
android:id="@+id/txt_article_url"/>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLength="5"
style="@style/small_textstyle"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:id="@+id/txt_article_datetime"/>
</RelativeLayout>
Nel layout appena riportato sono presenti riferimenti a stili e drawable di sfondo di cui non viene offerto qui il listato ma che ricalcano perfettamente ciò che è stato spiegato in merito in questa guida. Importante ricordare che questo layout, rappresenta la forma di una singola riga che apparirà nell'AdapterView.
L'elemento che si occuperà della trasformazione di ogni oggetto ArticlesInfo in una View sarà proprio l'Adapter: ma non uno standard, uno personalizzato creato da noi.
Per fare questo dobbiamo:
- Creare una classe che chiameremo
ArticlesAdapter
, estensione diBaseAdapter
; - Fare in modo che la classe possieda un riferimento alla struttura dati da visualizzare, magari passato tramite costruttore. Nel nostro caso sarà una List
<ArticlesInfo>
; - Implementare obbligatoriamente i metodi astratti di BaseAdapter:
getCount()
: restituisce la quantità di elementi presenti nella struttura dati;getItem(int position)
: restituisce un Object che rappresenta l'elemento della struttura dati nella posizione position;getItemId(int position)
: restituisce un id univoco per l'elemento in posizione position;getView(int position, View v, ViewGroup vg)
: restituisce la View che costituirà una riga della ListView. La View sarà struttura secondo il layout listactivity_row_article e popolata con i dati dell'elemento in posizione position.
Vediamo il codice dell'Adapter:
public class ArticlesAdapter extends BaseAdapter {
private List<ArticleInfo> articles=null;
private Context context=null;
private SimpleDateFormat simple=new SimpleDateFormat("dd/MM", Locale.ITALIAN);
public ArticlesAdapter(Context context,List<ArticleInfo> articles)
{
this.articles=articles;
this.context=context;
}
@Override
public int getCount()
{
return articles.size();
}
@Override
public Object getItem(int position)
{
return articles.get(position);
}
@Override
public long getItemId(int position)
{
return position;
}
@Override
public View getView(int position, View v, ViewGroup vg)
{
if (v==null)
{
v= LayoutInflater.from(context).inflate(R.layout.listactivity_row_article, null);
}
ArticleInfo ai=(ArticleInfo) getItem(position);
TextView txt=v.findViewById(R.id.txt_article_description);
txt.setText(ai.getTitle());
txt=v.findViewById(R.id.txt_article_url);
txt.setText(ai.getCategory());
txt=v.findViewById(R.id.txt_article_datetime);
txt.setText(simple.format(ai.getDate()));
return v;
}
}
Come si può vedere, i metodi non sono particolarmente complicati ma su getView vale la pena soffermarsi un attimo.
Al suo interno, per prima cosa, viene controllato se la View passata in input è nulla e solo in questo caso viene inizializzata con il LayoutInflater
. Questo aspetto è molto importante ai fini della salvaguardia delle risorse infatti Android riciclerà quanto possibile le View già create. Il LayoutInflater attua per i layout quello che abbiamo già visto fare per i menu con il MenuInflater
. In pratica la View da creare verrà strutturata in base al “progetto” definito nel layout XML indicatogli.
Dopo il blocco if
, la View non sarà sicuramente nulla perciò procederemo al completamento dei suoi campi. I dati verranno prelevati dall'oggetto ArticleInfo di posizione position recuperato mediante getItem
, già implementato. Al termine, getView
restituirà la View realizzata.
Questo Adapter incarnerà tutta la logica di trasformazione infatti per il resto l'Activity è molto semplice. Tra l'altro, estende la classe ListActivity che ha un layout costituito da una ListView e alcuni metodi per la sua gestione:
getListView()
per recuperare un riferimento alla ListView;setListAdapter()
egetListAdapter()
per ottenere accesso all'Adapter.
public class MainActivity extends ListActivity
{
private ArticlesAdapter adapter=new ArticlesAdapter(this, generateNews());
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
getListView().setPadding(10, 10, 10, 10);
setListAdapter(adapter);
}
private List<ArticleInfo> generateNews()
{
ArrayList<ArticleInfo> list=new ArrayList<ArticleInfo>();
Calendar c=Calendar.getInstance();
ArticleInfo tmp=new ArticleInfo();
tmp.setTitle("WordPress: integrare un pannello opzioni nel tema");
tmp.setCategory("CMS");
c.set(2021,3,23);
tmp.setDate(new Date(c.getTimeInMillis()));
list.add(tmp);
/*
* OMISSIS: il codice crea altri oggetti "fittizi" da visualizzare
* */
return list;
}
}
Come si è visto la realizzazione di un Adapter personalizzato non presenta eccessive difficoltà ma offre grandi potenzialità. Nel layout che struttura il singolo item dell'AdapterView si può inserire qualunque controllo visuale e ciò evita che il programmatore si trovi costretto a scendere a compromessi per la realizzazione della propria interfaccia utente.
Infine, nell'esempio si è fatto uso di una ListView ma un Adapter custom può lavorare con qualunque AdapterView.