[{"data":1,"prerenderedAt":75},["ShallowReactive",2],{"tutorial-liste-infinite-con-paginazione-in-flutter-usando-listview-e-scrollcontroller":3,"comments-tutorial-liste-infinite-con-paginazione-in-flutter-usando-listview-e-scrollcontroller":74},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"video_url":10,"difficulty":11,"estimated_minutes":12,"flutter_version":13,"status":14,"published_at":15,"meta_title":16,"meta_description":17,"category":18,"author":22,"steps":24},19,"Liste infinite con paginazione in Flutter usando ListView e ScrollController","liste-infinite-con-paginazione-in-flutter-usando-listview-e-scrollcontroller","Impara a creare una lista che carica automaticamente nuovi dati man mano che l'utente scorre, implementando la paginazione (infinite scroll) con ListView.builder e ScrollController.","Quando un'app deve mostrare grandi quantità di dati provenienti da un'API, caricare tutto in una volta è inefficiente e lento. La soluzione è la **paginazione**, ovvero il caricamento progressivo dei dati a blocchi (pagine) man mano che l'utente scorre la lista.\n\nIn questo tutorial costruiremo una lista a scorrimento infinito (*infinite scroll*) usando `ListView.builder` combinato con uno `ScrollController` per rilevare quando l'utente si avvicina alla fine della lista. Gestiremo anche gli stati di caricamento e la fine dei dati disponibili.\n\nNon useremo pacchetti esterni: tutto ciò che serve è già incluso in Flutter.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fc445d0b3-9c0b-4a30-9389-f2e6ab3577c1.jpg",null,"intermediate",35,"3.x","published","2026-06-25T04:30:45+00:00","Liste infinite in Flutter con paginazione e ScrollController","Guida pratica per creare liste a scorrimento infinito in Flutter con ListView.builder e ScrollController, con gestione del caricamento progressivo.",{"id":19,"name":20,"slug":21},1,"Guide","guide",{"id":19,"name":23},"Flutter Bot",[25,32,39,46,53,60,67],{"id":26,"position":19,"title":27,"body":28,"code_snippet":29,"code_language":30,"expected_result":31,"demo_url":10,"video_url":10},130,"Preparare il servizio dati simulato","Per concentrarci sulla logica di paginazione, simuliamo un'API che restituisce blocchi di dati. In un progetto reale questa funzione effettuerebbe una chiamata HTTP passando i parametri di pagina (`page`) e dimensione (`limit`).\n\nLa funzione introduce un ritardo artificiale per simulare la latenza di rete e restituisce 20 elementi per pagina, fermandosi dopo la pagina 5 per simulare la fine dei dati.","class ItemRepository {\n  static const int pageSize = 20;\n  static const int maxPages = 5;\n\n  Future\u003CList\u003CString>> fetchItems(int page) async {\n    \u002F\u002F Simula la latenza di rete\n    await Future.delayed(const Duration(seconds: 1));\n\n    \u002F\u002F Nessun dato oltre l'ultima pagina\n    if (page > maxPages) return [];\n\n    final start = (page - 1) * pageSize;\n    return List.generate(\n      pageSize,\n      (i) => 'Elemento ${start + i + 1}',\n    );\n  }\n}","dart","Una classe ItemRepository che restituisce 20 elementi per pagina, fino a 5 pagine.",{"id":33,"position":34,"title":35,"body":36,"code_snippet":37,"code_language":30,"expected_result":38,"demo_url":10,"video_url":10},131,2,"Creare lo StatefulWidget e le variabili di stato","Creiamo un `StatefulWidget` che conterrà la lista degli elementi caricati e le variabili necessarie a gestire la paginazione:\n\n- `_items`: la lista cumulativa degli elementi caricati;\n- `_currentPage`: la pagina corrente;\n- `_isLoading`: indica se è in corso un caricamento (evita richieste duplicate);\n- `_hasMore`: indica se ci sono altri dati da caricare;\n- `_scrollController`: controlla la posizione di scorrimento.","class InfiniteListPage extends StatefulWidget {\n  const InfiniteListPage({super.key});\n\n  @override\n  State\u003CInfiniteListPage> createState() => _InfiniteListPageState();\n}\n\nclass _InfiniteListPageState extends State\u003CInfiniteListPage> {\n  final ItemRepository _repository = ItemRepository();\n  final ScrollController _scrollController = ScrollController();\n\n  final List\u003CString> _items = [];\n  int _currentPage = 1;\n  bool _isLoading = false;\n  bool _hasMore = true;\n\n  @override\n  void dispose() {\n    _scrollController.dispose();\n    super.dispose();\n  }\n}","La struttura dello State con tutte le variabili necessarie alla paginazione.",{"id":40,"position":41,"title":42,"body":43,"code_snippet":44,"code_language":30,"expected_result":45,"demo_url":10,"video_url":10},132,3,"Implementare la logica di caricamento","Scriviamo il metodo `_loadItems` che recupera la pagina corrente e aggiunge gli elementi alla lista. \n\nÈ fondamentale controllare `_isLoading` e `_hasMore` all'inizio per evitare chiamate multiple sovrapposte. Quando l'API restituisce meno elementi di `pageSize` (o una lista vuota), impostiamo `_hasMore` a `false` per fermare ulteriori caricamenti.","Future\u003Cvoid> _loadItems() async {\n  if (_isLoading || !_hasMore) return;\n\n  setState(() => _isLoading = true);\n\n  final newItems = await _repository.fetchItems(_currentPage);\n\n  if (!mounted) return;\n\n  setState(() {\n    _items.addAll(newItems);\n    _isLoading = false;\n    _currentPage++;\n    if (newItems.length \u003C ItemRepository.pageSize) {\n      _hasMore = false;\n    }\n  });\n}","Un metodo che carica una pagina alla volta e aggiorna correttamente lo stato.",{"id":47,"position":48,"title":49,"body":50,"code_snippet":51,"code_language":30,"expected_result":52,"demo_url":10,"video_url":10},133,4,"Rilevare lo scroll e caricare la prima pagina","Nel metodo `initState` carichiamo la prima pagina e aggiungiamo un listener allo `ScrollController`. Il listener controlla la distanza dal fondo della lista: quando l'utente è a meno di 200 pixel dalla fine, avviamo il caricamento della pagina successiva.\n\nUsare una soglia (in questo caso 200px) anziché aspettare il fondo esatto rende lo scorrimento più fluido, perché i nuovi dati vengono caricati in anticipo.","@override\nvoid initState() {\n  super.initState();\n  _loadItems();\n  _scrollController.addListener(_onScroll);\n}\n\nvoid _onScroll() {\n  final position = _scrollController.position;\n  if (position.pixels >= position.maxScrollExtent - 200) {\n    _loadItems();\n  }\n}","La prima pagina viene caricata all'avvio e lo scroll attiva i caricamenti successivi.",{"id":54,"position":55,"title":56,"body":57,"code_snippet":58,"code_language":30,"expected_result":59,"demo_url":10,"video_url":10},134,5,"Costruire la UI con ListView.builder","Ora costruiamo l'interfaccia. Usiamo `ListView.builder` collegato allo `_scrollController`. \n\nIl conteggio degli elementi (`itemCount`) include un elemento extra in fondo quando `_hasMore` è `true`: questo elemento mostra un indicatore di caricamento. Quando l'indice supera la lunghezza della lista, mostriamo il `CircularProgressIndicator`, altrimenti la riga con il dato.","@override\nWidget build(BuildContext context) {\n  return Scaffold(\n    appBar: AppBar(title: const Text('Lista infinita')),\n    body: ListView.builder(\n      controller: _scrollController,\n      itemCount: _items.length + (_hasMore ? 1 : 0),\n      itemBuilder: (context, index) {\n        if (index >= _items.length) {\n          return const Padding(\n            padding: EdgeInsets.all(16),\n            child: Center(child: CircularProgressIndicator()),\n          );\n        }\n        return ListTile(\n          leading: CircleAvatar(child: Text('${index + 1}')),\n          title: Text(_items[index]),\n        );\n      },\n    ),\n  );\n}","Una lista scorrevole che mostra gli elementi e un indicatore di caricamento in fondo durante il fetch.",{"id":61,"position":62,"title":63,"body":64,"code_snippet":65,"code_language":30,"expected_result":66,"demo_url":10,"video_url":10},135,6,"Aggiungere il pull-to-refresh (opzionale)","Per migliorare l'esperienza utente, avvolgiamo la `ListView` in un `RefreshIndicator` che permette di ricaricare la lista dall'inizio trascinandola verso il basso.\n\nIl metodo `_refresh` azzera tutte le variabili di stato e ricarica la prima pagina.","Future\u003Cvoid> _refresh() async {\n  setState(() {\n    _items.clear();\n    _currentPage = 1;\n    _hasMore = true;\n  });\n  await _loadItems();\n}\n\n\u002F\u002F Nel build, avvolgi la ListView:\n\u002F\u002F body: RefreshIndicator(\n\u002F\u002F   onRefresh: _refresh,\n\u002F\u002F   child: ListView.builder( ... ),\n\u002F\u002F ),","Trascinando la lista verso il basso, i dati vengono ricaricati dall'inizio.",{"id":68,"position":69,"title":70,"body":71,"code_snippet":72,"code_language":30,"expected_result":73,"demo_url":10,"video_url":10},136,7,"Gestire la fine dei dati e gli errori","Come tocco finale, miglioriamo l'esperienza mostrando un messaggio quando non ci sono più dati e gestendo eventuali errori di rete con un blocco `try\u002Fcatch`.\n\nIn produzione è buona pratica salvare lo stato di errore e mostrare un pulsante \"Riprova\" per ritentare il caricamento della pagina fallita.","Future\u003Cvoid> _loadItems() async {\n  if (_isLoading || !_hasMore) return;\n  setState(() => _isLoading = true);\n\n  try {\n    final newItems = await _repository.fetchItems(_currentPage);\n    if (!mounted) return;\n    setState(() {\n      _items.addAll(newItems);\n      _currentPage++;\n      if (newItems.length \u003C ItemRepository.pageSize) _hasMore = false;\n    });\n  } catch (e) {\n    if (mounted) {\n      ScaffoldMessenger.of(context).showSnackBar(\n        SnackBar(content: Text('Errore di caricamento: $e')),\n      );\n    }\n  } finally {\n    if (mounted) setState(() => _isLoading = false);\n  }\n}","La lista gestisce correttamente la fine dei dati e mostra una SnackBar in caso di errore.",[],1785219611119]