In questa lezione, focalizzeremo la nostra attenzione su uno degli elementi principe dello sviluppo di qualsiasi applicazione mobile che debba mostrare un elenco di elementi tra cui l’utente può scegliere: la ListView
.
La classe ListView
Come visto finora, qualsiasi componente grafico in Flutter è di base un Widget
, anche la ListView
. Definito come uno scrolling widget, la ListView
mostra un insieme di widget figli uno dopo l’altro secondo la direzione di scorrimento del listato.
Una delle peculiarità di questo widget è la possibilità di definire una ListView
attraverso diverse tipologie di costruttori, lasciando così ampio respiro allo sviluppatore che può scegliere quello più congeniale alle proprie esigenze.
Vediamo quindi quali sono i costruttori a nostra disposizione e quando usarli.
Costruttore | Descrizione |
---|---|
ListView |
crea un array di Widget scorrevole e lineare a partire da un oggetto List .
È opportuno utilizzare questo costruttore quando bisogna mostrare all’utente una lista contenuta di elementi, in quanto la creazione di un oggetto Al suo posto, è consigliabile usare il costruttore |
ListView.builder |
questo costruttore crea un array scorrevole e lineare di widget creati su richiesta.
Differentemente dal costruttore Questo costruttore accetta in input diversi parametri, tra cui quello di maggior interesse è Questo costruttore, inoltre, non supporta di default il riordinamento degli elementi in un secondo momento. In questo caso è consigliabile usare i costruttori |
ListView.custom |
Crea un array scorrevole e lineare di widget sulla base delle funzionalità definite dallo sviluppatore per la creazione degli elementi della lista.
Per farlo, il costruttore fa uso di un
In particolare possiamo affermare che:
|
ListView.separated |
definisce un array scorrevole e lineare a lunghezza fissa dove gli elementi sono intervallati da una lista di elementi che fungono da separatori.
I separatori vengono visualizzati solo tra gli elementi dell’elenco, ossia il primo separatore comparirà dopo il primo elemento e l’ultimo separatore verrà mostrato prima dell’ultimo elemento. Per creare sia gli elementi sia i separatori sono definite due
Questo costruttore (come il |
Analizzati i possibili costruttori, facciamo una panoramica delle principali proprietà di questo importante widget.
Proprietà | Tipo Accettato | Descrizione |
---|---|---|
childrenDelegate |
SilverChildDelegate |
rappresenta un delegato che fornisce la lista di elementi alla ListView . Ciò è possibile con il costruttore ListView.custom , mentre i costruttori ListView e ListView.builder ne creano uno contenente la lista di elementi e la callback IndexedWidgetBuilder |
itemExtent |
double |
se definito, forza gli elementi della lista ad avere un’estensione nella direzione dello scroll. Definendo questa proprietà, il framework è in grado di ottimizzare le operazioni di rendering al cambio della posizione dello scroll |
controller |
ScrollController |
definisce un oggetto ScrollController utilizzato per controllare la posizione in cui viene fatta scorrere la lista. Questo tipo di oggetto è utile per molteplici motivi, tra cui la possibilità di controllare la posizione iniziale dello scroll e quando deve essere salvata la posizione in automatico per ripristinarla successivamente. |
padding |
EdgeInsetsGeometry |
definisce lo spazio in cui collocare gli elementi |
physics |
ScrollPhysics |
è uno dei parametri più interessanti, in quanto definisce come la scrollView debba rispondere all’input dell’utente, come ad esempio il comportamento dello scroll dopo che l’utente smette di scorrere la lista |
primary |
bool |
se impostata a false e se il numero di elementi è insufficiente a scorrere la schermata, allora l’utente non può effettuare lo scroll. |
Per una visione completa di tutte le proprietà e ulteriori dettagli sui costruttori si rimanda alla documentazione ufficiale.
Esempi pratici
Entriamo nel vivo di questa lezione vedendo nel dettaglio alcuni esempi di creazione delle ListView
utilizzando alcuni dei costruttori precedentemente illustrati.
Per iniziare, creiamo un nuovo progetto come mostrato nella lezione 6 di questa guida e, lasciando inalterati gli import, il metodo main()
e la classe MyApp
, cancelliamo il resto per aggiungere il seguente StatelessWidget
, composto da uno Scaffold
per la definizione dell’AppBar
e la proprietà body
che andremo a impostare di volta in volta con la definizione di un nuovo tipo di ListView
.
class MyListViews extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Lesson 18'),
),
body: /*Set the desired ListView*/ ,
);
}
. . .
}
Inizializzato il progetto, cominciamo utilizzando il costruttore più semplice messo a disposizione per noi, ossia ListView
.
Creiamo quindi una nuova funzione all’interno della classe MyListView
, che si chiamerà _myListView()
e che conterrà semplicemente una lista di Widget
di tipo ListTitle
per la definizione di una singola riga della lista di altezza fissa. Grazie a questo Widget
è possibile definire un testo, un sottotesto e un widget da impostare all’inizio o alla fine della riga tramite le proprietà:
leading
, che permette di mostrare un widget prima del titolo;trailing
, che permette di mostrare un widget dopo il titolo.
A questo si aggiunge anche un’altra proprietà importante, che vedremo più avanti con un esempio dedicato, ossia onTap
che permette di definire una GestureTapCallback
per intercettare e gestire il click dell’utente sull’elemento della lista.
In questo esempio la lista di ListTitle
conterrà semplicemente il titolo definito tramite la proprietà title
.
class MyListViews extends StatelessWidget {
Widget _myListView() {
return ListView(
children: <Widget>[
ListTile(
title: Text('Article number 1'),
),
ListTile(
title: Text('Article number 2'),
),
ListTile(
title: Text('Article number 3'),
),
ListTile(
title: Text('Article number 4'),
),
ListTile(
title: Text('Article number 5'),
),
ListTile(
title: Text('Article number 6'),
),
ListTile(
title: Text('Article number 7'),
),
ListTile(
title: Text('Article number 8'),
),
ListTile(
title: Text('Article number 9'),
),
ListTile(
title: Text('Article number 10'),
),
ListTile(
title: Text('Article number 11'),
),
ListTile(
title: Text('Article number 12'),
),
],
);
}
Invochiamo questa funzione impostandola come valore della proprietà body
ed eseguiamo l’applicazione.
Figura 114. Creazione di una ListView
di 12 elementi con il costruttore ListView
per a) Android e b) iOS
Come si può vedere, abbiamo ottenuto in modo semplice e veloce una lista di elementi predefiniti, ognuno di questi con il proprio titolo. Ciononostante, la lista non risulta esteticamente gradevole in quanto gli elementi sembrano essere contenuti in un’unica riga a causa della mancanza di un separatore.
Per rimediare al problema, possiamo utilizzare il metodo ListTitle.divideTiles
che aggiunge un bordo di un pixel di dimensione tra ogni riga utilizzando il colore di default fornito dal tema, laddove non definito. Questo metodo restituisce un Iterable<Widget>
che deve essere convertito in una lista con il metodo toList()
per poter essere impostato come valore della proprietà children di ListView
.
Vediamo di seguito l’esempio.
Widget _myListViewWithBasicSeparator(BuildContext context) {
return ListView(
children: ListTile.divideTiles(
context: context,
tiles: [
ListTile(
title: Text('Article number 1'),
),
ListTile(
title: Text('Article number 2'),
),
ListTile(
title: Text('Article number 3'),
),
ListTile(
title: Text('Article number 4'),
),
ListTile(
title: Text('Article number 5'),
),
ListTile(
title: Text('Article number 6'),
),
ListTile(
title: Text('Article number 7'),
),
ListTile(
title: Text('Article number 8'),
),
ListTile(
title: Text('Article number 9'),
)
],
).toList());
}
Inoltre, come è possibile notare esaminando attentamente il metodo appena scritto, abbiamo impostato per la proprietà contex
di ListTitle.divideTiles
con il BuildContext
, un handler per la posizione di un widget nel widget tree.
Impostiamo il valore della proprietà body
con il valore di ritorno fornito dal metodo appena creato e otterremo il seguente risultato una volta eseguita l’applicazione.
Figura 115. Creazione di una ListView
con un separatore tramite il costruttore ListView
per a) Android e b) iOS
Il risultato ottenuto sarà esattamente uguale al precedente, con la differenza che questa volta tra gli elementi è presente un separatore grigio scuro.
Come accennato all’inizio di questa lezione, il costruttore ListView
è molto pratico nel momento in cui abbiamo a che fare con una lista finita di elementi, ma per liste potenzialmente infinite occorre utilizzare un altro costruttore, ListView.builder
.
Vediamolo subito definendo un nuovo metodo che si chiamerà _myListViewBuilder
e che prenderà come parametro di input il BuildContex
utilizzato dall’itemBuilder
per creare dinamicamente ogni ListTitle
, aggiungendo nel titolo l’indice corrente della ListTitle
. Vediamo come.
Widget _myListViewBuilder(BuildContext context) {
return ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('Article number $index'),
);
},
);
}
In questo modo abbiamo creato una lista infinita di elementi, come si può vedere dalla figura di seguito.
Figura 116a. Creazione di una ListView
infinita tramite il costruttore ListView.builder
per Android
Figura 116b. Creazione di una ListView
infinita tramite il costruttore ListView.builder
per iOS
In tutti i framework di sviluppo di applicazioni è spesso necessario dover definire una lista a scorrimento orizzontale, e in questo caso è necessario specificare la direzione dello scroll tramite la proprietà scrollDirection
impostando l’asse a orizzontale come fatto in questo esempio.
Widget _myListViewBuilderHorizontal(BuildContext context) {
return ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 50.0,
),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
width: 50,
color: Colors.deepOrangeAccent,
child: Center(child: Text('$index')),
);
},
));
}
In questo caso, abbiamo definito la nostra lista come elemento figlio di un ConstrainedBox
per evitare che la lista si espanda verticalmente.
Impostiamo il Widget di ritorno della funzione come valore della proprietà body ed eseguiamo l’applicazione per ottenere il seguente risultato.
Figura 117a. Creazione di una ListView
orizzontale e infinita tramite il costruttore ListView.builder
per Android
Figura 117b. Creazione di una ListView
orizzontale e infinita tramite il costruttore ListView.builder
per iOS
Adesso che abbiamo preso confidenza con le ListView
e il costruttore builder
, rendiamo la nostra lista più carina e creiamo una lista composta da 9 elementi, ognuno dei quali con un’icona e un testo.
Creiamo quindi un nuovo metodo che si chiamerà _myListViewWithCustomIconAndCard
e che prende in input il BuildContext
necessario all’itemBuilder
per costruire il listato.
Successivamente, definiamo due liste di elementi che conterranno le icone e i testi da mostrare associati e definiamo la nostra ListView
come segue.
Widget _myListViewWithCustomIconAndCard(BuildContext context) {
final titles = [
'alarm',
'pics',
'PDF collection',
'camera',
'giftcard',
'edit',
'adb',
'zoom in',
'zoom out'
];
final icons = [
Icons.access_alarm,
Icons.collections,
Icons.picture_as_pdf,
Icons.camera,
Icons.card_giftcard,
Icons.mode_edit,
Icons.adb,
Icons.zoom_in,
Icons.zoom_out
];
return ListView.builder(
itemCount: titles.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: Icon(icons[index], color: Colors.orange[800], size: 20),
title: Text(titles[index]),
),
elevation: 3,
shape: StadiumBorder(
side: BorderSide(
color: Colors.deepOrange,
width: 1.0,
)));
},
);
}
In particolare, nel corpo della funzione associata all’itemBuilder
abbiamo definito un Card
widget il cui figlio è un ListTitle
con le proprietà leading
e title
impostate ai valori delle due liste tramite l’indice corrente. Inoltre, abbiamo personalizzato le nostre Card
definendo una elevazione e una forma attraverso le proprietà elevation
e shape
, rispettivamente.
Impostiamo,quindi, il valore della proprietà body
con il valore di ritorno fornito dal metodo appena creato e otterremo il seguente risultato.
Figura 118. Creazione di una ListView
personalizzata tramite il costruttore ListView.builder
per a) Android b) iOS
Se agli elementi della lista volessimo aggiungere la possibilità di catturare l’evento touch che scaturisce nel momento in cui l’utente tocca uno degli elementi, basterà aggiungere la proprietà onTap
definendo l’azione desiderata. In questo caso, immaginiamo di voler semplicemente stampare in console il titolo dell’elemento e modifichiamo il nostro ListTitle
come nell’esempio seguente.
Widget _myListViewWithCustomIconCardAndTouchEvent(BuildContext context) {
// . . .
return Card(
child: ListTile(
leading: Icon(icons[index], color: Colors.orange[800], size: 20),
title: Text(titles[index]),
onTap: () {
print(titles[index]);
},
),
// . . .
);
//. . .
}
Modifichiamo quindi la proprietà body
con il nuovo metodo scritto ed eseguiamo l’app.
L’applicazione è rimasta inalterata graficamente, ma ogni volta che toccheremo uno degli elementi della lista verrà stampato il relativo titolo nella console, come possiamo vedere in questo caso.
Figura 119. Output della console ad ogni click di un elemento della lista
All’inizio di questa sezione ci siamo posti il problema di rendere netta la separazione tra gli elementi della lista, e per farlo abbiamo usato il metodo ListTitle.devideTiles
. In realtà, come abbiamo visto all’inizio della lezione, possiamo (e dobbiamo) usare il costruttore ListView.separated
che offre un maggiore controllo sulla creazione e sul contenuto del separatore.
Immaginiamo di voler creare una lista di 50 elementi composti da un titolo, sottotitolo e un’icona e intervalliamo questi elementi da un separatore contenente un testo equivalente che mostri l’indice corrente del separatore.
Per farlo, è necessario definire, oltre all’itemBuilder
, anche la proprietà separatorBuilder
di ListView
e l’itemCount
per far si che la ListView
conosca la lunghezza della lista per attuare ottimizzazioni.
Creiamo quindi un nuovo metodo che si chiamerà _myListViewSeparated
e che prende in input il BuildContex
da passare sia al separatorBuilder
che all’itemBuilder
. Vediamo come.
Widget _myListViewSeparated(BuildContext context) {
return ListView.separated(
itemCount: 50,
separatorBuilder: (context, int index) {
return Container(
child: ListTile(
title: Text(
'SeperatorItem $index',
style: TextStyle(color: Colors.white),
)),
color: Colors.red[900],
margin: EdgeInsets.symmetric(vertical: 10),
);
},
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
leading: const Icon(Icons.accessibility,
size: 40.0, color: Colors.white),
title: Text('Title $index'),
subtitle: Text('SubText'),
),
color: Colors.amber,
);
},
);
}
Aggiorniamo quindi il valore della proprietà body
ed eseguiamo l’app ottenendo quanto segue.
Figura 120. Creazione di una ListView
definita tramite il costruttore ListView.separator
per a) Android b) iOS
Come possiamo vedere scorrendo la lista, gli elementi sono intervallati da separatori il cui numero è pari ai itemCount-1
. Questo diventa chiaro scorrendo fino alla fine della lista, dove l’ultimo elemento non è un separatore ma un item costruito dall’itemBuilder
e l’ultimo indice dei separatori è pari a 48.
Ovviamente, non è necessario dover inserire del testo all’interno del separatore e possiamo costruirlo anche in modo più semplice facendo tornare al separatorBuilder
un Divider
, come mostrato nell’esempio seguente.
Widget _myListViewSeparatedBasic(BuildContext context) {
return ListView.separated(
itemCount: 50,
separatorBuilder: (BuildContext context, int index) {
return Divider(
color: Colors.red[900],
thickness: 5,
);
},
// . . .
);
}
Lasciando quindi il resto del codice inalterato e modificando solo il separatorBuilder
otterremo quanto segue.
Figura 121. Definizione di un semplice separatore per il costruttore ListView.separator
Il codice di questa lezione è disponibile su GitHub.