Introduzione
Quando un'app deve mostrare grandi quantità di dati provenienti da un backend, caricare tutto in una volta è inefficiente e dannoso per le performance e per l'esperienza utente. La soluzione più diffusa è la paginazione infinita (o infinite scroll): i dati vengono caricati a blocchi man mano che l'utente scorre la lista.
Implementare questa logica a mano richiede di gestire lo stato di caricamento, gli errori, la pagina corrente, il rilevamento della fine della lista e i casi limite. Il pacchetto infinite_scroll_pagination semplifica enormemente tutto questo.
Installazione
Aggiungi la dipendenza al pubspec.yaml:
dependencies:
infinite_scroll_pagination: ^4.0.0
Poi esegui flutter pub get.
Concetti fondamentali
Il cuore del pacchetto è il PagingController, che gestisce lo stato della lista paginata. Espone:
- itemList: gli elementi caricati finora
- nextPageKey: la chiave (pagina, cursore, offset) della prossima pagina
- error: eventuale errore di caricamento
I widget PagedListView, PagedGridView e PagedSliverList si occupano di renderizzare la lista e di richiamare il caricamento quando serve.
Un esempio completo
Supponiamo di avere un servizio che restituisce una lista paginata di articoli.
import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
class Articolo {
final int id;
final String titolo;
Articolo({required this.id, required this.titolo});
}
class ArticoliPage extends StatefulWidget {
const ArticoliPage({super.key});
@override
State<ArticoliPage> createState() => _ArticoliPageState();
}
class _ArticoliPageState extends State<ArticoliPage> {
static const _pageSize = 20;
final PagingController<int, Articolo> _pagingController =
PagingController(firstPageKey: 0);
@override
void initState() {
super.initState();
_pagingController.addPageRequestListener((pageKey) {
_fetchPage(pageKey);
});
}
Future<void> _fetchPage(int pageKey) async {
try {
final nuoviArticoli = await _apiFetch(pageKey, _pageSize);
final isLastPage = nuoviArticoli.length < _pageSize;
if (isLastPage) {
_pagingController.appendLastPage(nuoviArticoli);
} else {
final nextPageKey = pageKey + nuoviArticoli.length;
_pagingController.appendPage(nuoviArticoli, nextPageKey);
}
} catch (error) {
_pagingController.error = error;
}
}
// Simulazione di una chiamata API
Future<List<Articolo>> _apiFetch(int offset, int limit) async {
await Future.delayed(const Duration(seconds: 1));
return List.generate(
limit,
(i) => Articolo(id: offset + i, titolo: 'Articolo ${offset + i}'),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Articoli')),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PagedListView<int, Articolo>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Articolo>(
itemBuilder: (context, articolo, index) => ListTile(
title: Text(articolo.titolo),
),
firstPageErrorIndicatorBuilder: (_) => Center(
child: TextButton(
onPressed: () => _pagingController.refresh(),
child: const Text('Errore, riprova'),
),
),
noItemsFoundIndicatorBuilder: (_) =>
const Center(child: Text('Nessun articolo')),
),
),
),
);
}
@override
void dispose() {
_pagingController.dispose();
super.dispose();
}
}
Cosa succede
- Il
PagingControllerviene inizializzato confirstPageKey: 0. - Registriamo un listener che viene invocato ogni volta che serve caricare una pagina.
- Se il numero di elementi ricevuti è inferiore a
_pageSize, siamo all'ultima pagina e usiamoappendLastPage; altrimentiappendPagecon la chiave successiva. - In caso di errore assegniamo
_pagingController.error, e il widget mostra l'indicatore di errore.
Pull-to-refresh
Avvolgendo il PagedListView in un RefreshIndicator e chiamando _pagingController.refresh() è possibile ricaricare l'intera lista dal principio, molto utile per aggiornare i contenuti.
Gestire i filtri e la ricerca
Quando cambia un filtro o una query di ricerca è sufficiente chiamare _pagingController.refresh() dopo aver aggiornato lo stato del filtro. Il controller ripartirà dalla firstPageKey usando i nuovi parametri all'interno di _fetchPage.
void onSearchChanged(String query) {
_query = query;
_pagingController.refresh();
}
PagedGridView e Slivers
Oltre alle liste, il pacchetto offre PagedGridView per griglie e PagedSliverList / PagedSliverGrid per integrare la paginazione all'interno di un CustomScrollView, ad esempio con header collassabili.
Best practice
- Dimensione della pagina adeguata: 15-25 elementi è un buon compromesso tra numero di richieste e quantità di dati.
- Debounce sulla ricerca: evita di chiamare
refresh()ad ogni carattere digitato. - Gestione degli errori chiara: fornisci sempre
firstPageErrorIndicatorBuilderenewPageErrorIndicatorBuilder. - Dispose del controller: ricordati sempre di rilasciarlo nel
dispose().
Conclusione
Con infinite_scroll_pagination puoi implementare liste paginate robuste con poche righe di codice, delegando al pacchetto la gestione degli stati più complessi. Combinato con Dio o Retrofit per le chiamate di rete e con un pattern di gestione dello stato come Riverpod o BLoC, diventa uno strumento essenziale per app che consumano API con grandi dataset.
