[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-controllare-lo-scroll-in-flutter-scrollcontroller-scrollphysics-e-scrollnotification":3,"comments-article-controllare-lo-scroll-in-flutter-scrollcontroller-scrollphysics-e-scrollnotification":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},109,"Controllare lo scroll in Flutter: ScrollController, ScrollPhysics e ScrollNotification","controllare-lo-scroll-in-flutter-scrollcontroller-scrollphysics-e-scrollnotification","Scorrere una lista è facile, controllarla davvero un po' meno. Guida pratica a ScrollController, ScrollNotification e ScrollPhysics personalizzate per creare snap, scroll-to-top, header dinamici e trigger di caricamento affidabili.","Quasi ogni app Flutter è fatta di superfici scorrevoli: liste, griglie, form lunghi, caroselli. Finché serve solo mostrare contenuti, `ListView` basta e avanza. I problemi arrivano quando bisogna **controllare** lo scroll: tornare in cima con un tasto, nascondere una bottom bar mentre l'utente scorre, agganciare un carosello alla card successiva, caricare altri dati quando si arriva in fondo.\n\nIn questa guida vediamo i tre strumenti che governano lo scorrimento in Flutter — `ScrollController`, `ScrollNotification` e `ScrollPhysics` — con esempi pronti all'uso e le trappole più comuni.\n\n## L'anatomia di uno scrollable\n\nDietro a un `ListView` ci sono tre attori:\n\n- **`Scrollable`**: il widget che riceve i gesti e trasforma i drag in offset.\n- **`ScrollPosition`**: lo stato dello scroll (offset corrente, estensioni minime e massime, direzione dell'utente). Contiene le *metriche* (`ScrollMetrics`).\n- **`ScrollController`**: l'oggetto che si attacca a una o più `ScrollPosition` e permette di leggerle e pilotarle dall'esterno.\n\nCapire questa separazione evita il 90% degli errori: il controller **non** è lo scroll, è solo un telecomando che può essere collegato a zero, uno o più `Scrollable`.\n\n## ScrollController: leggere e pilotare\n\n```dart\nclass ProductList extends StatefulWidget {\n  const ProductList({super.key});\n\n  @override\n  State\u003CProductList> createState() => _ProductListState();\n}\n\nclass _ProductListState extends State\u003CProductList> {\n  final _controller = ScrollController();\n  bool _showFab = false;\n\n  @override\n  void initState() {\n    super.initState();\n    _controller.addListener(_onScroll);\n  }\n\n  void _onScroll() {\n    final shouldShow = _controller.offset > 400;\n    if (shouldShow != _showFab) {\n      setState(() => _showFab = shouldShow);\n    }\n  }\n\n  @override\n  void dispose() {\n    _controller.removeListener(_onScroll);\n    _controller.dispose(); \u002F\u002F obbligatorio: è un ChangeNotifier\n    super.dispose();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      body: ListView.builder(\n        controller: _controller,\n        itemCount: 200,\n        itemBuilder: (context, i) => ListTile(title: Text('Prodotto $i')),\n      ),\n      floatingActionButton: _showFab\n          ? FloatingActionButton(\n              onPressed: () => _controller.animateTo(\n                0,\n                duration: const Duration(milliseconds: 400),\n                curve: Curves.easeOutCubic,\n              ),\n              child: const Icon(Icons.arrow_upward),\n            )\n          : null,\n    );\n  }\n}\n```\n\nPunti chiave:\n\n- `jumpTo()` sposta istantaneamente, `animateTo()` anima con curva e durata.\n- Il listener del controller viene invocato **a ogni frame di scroll**: fai il minimo indispensabile e chiama `setState` solo quando il valore derivato cambia davvero (come nell'esempio sopra), altrimenti ricostruisci l'intera pagina 60 volte al secondo.\n- `dispose()` è obbligatorio, pena un memory leak segnalato anche da `leak_tracker`.\n\n### Attenzione a `controller.position`\n\nLeggere `_controller.position` o `_controller.offset` **prima** che il widget sia stato montato lancia un'eccezione (`ScrollController not attached to any scroll views`). Se ti serve l'offset subito dopo il primo build, usa un post-frame callback:\n\n```dart\nWidgetsBinding.instance.addPostFrameCallback((_) {\n  if (!_controller.hasClients) return;\n  _controller.jumpTo(_controller.position.maxScrollExtent);\n});\n```\n\nE ricorda: un controller collegato a più `Scrollable` (per esempio dentro un `TabBarView` con pagine riciclate) fa fallire `position`; in quel caso usa `controller.positions` o, molto meglio, un controller per ogni lista.\n\n## ScrollNotification: reagire senza listener continui\n\nQuando la reazione allo scroll riguarda un sottoalbero (e non ti serve pilotare lo scroll), `NotificationListener` è più elegante di un controller: cattura le notifiche che risalgono l'albero, anche da liste annidate.\n\n```dart\nNotificationListener\u003CScrollNotification>(\n  onNotification: (notification) {\n    if (notification.depth != 0) return false; \u002F\u002F ignora liste annidate\n\n    if (notification is UserScrollNotification) {\n      switch (notification.direction) {\n        case ScrollDirection.reverse:\n          _bottomBar.hide();\n        case ScrollDirection.forward:\n          _bottomBar.show();\n        case ScrollDirection.idle:\n          break;\n      }\n    }\n    return false; \u002F\u002F false = lascia propagare la notifica\n  },\n  child: const ProductList(),\n)\n```\n\nI tipi principali sono:\n\n- `ScrollStartNotification` \u002F `ScrollEndNotification`: inizio e fine di un'attività di scroll (drag o inerzia).\n- `ScrollUpdateNotification`: ogni variazione di offset, con `scrollDelta`.\n- `OverscrollNotification`: l'utente ha tirato oltre il bordo (utile per pull-to-refresh custom).\n- `UserScrollNotification`: cambia la direzione dell'interazione utente.\n\n### Trigger di paginazione affidabile\n\nUn errore classico è caricare la pagina successiva dentro il listener del controller: la condizione diventa vera per decine di frame consecutivi e scattano chiamate duplicate. Usare `ScrollEndNotification` più una guardia è molto più solido:\n\n```dart\nbool _loading = false;\n\nFuture\u003Cvoid> _maybeLoadMore(ScrollMetrics m) async {\n  if (_loading) return;\n  if (m.extentAfter > 600) return; \u002F\u002F mancano più di 600px alla fine\n  _loading = true;\n  try {\n    await context.read\u003CProductsRepository>().loadNextPage();\n  } finally {\n    _loading = false;\n  }\n}\n\n\u002F\u002F ...\nNotificationListener\u003CScrollEndNotification>(\n  onNotification: (n) {\n    _maybeLoadMore(n.metrics);\n    return false;\n  },\n  child: list,\n)\n```\n\nLe `ScrollMetrics` offrono tutto quello che serve: `pixels`, `maxScrollExtent`, `extentBefore`, `extentAfter`, `atEdge`, `outOfRange`, `viewportDimension`.\n\n## ScrollPhysics: cambiare il \"feeling\" dello scroll\n\nLe physics decidono come l'offset risponde al dito e come si comporta l'inerzia. Le più usate:\n\n- `BouncingScrollPhysics`: rimbalzo stile iOS.\n- `ClampingScrollPhysics`: stop netto ai bordi, stile Android.\n- `AlwaysScrollableScrollPhysics`: permette il drag anche quando il contenuto è più corto del viewport (indispensabile con `RefreshIndicator`).\n- `NeverScrollableScrollPhysics`: disabilita lo scroll, tipico per liste annidate dentro un `SingleChildScrollView`.\n- `PageScrollPhysics`: aggancio a pagine intere, quello usato da `PageView`.\n\nSi combinano tramite il parametro `parent`:\n\n```dart\nconst AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics())\n```\n\n### Physics personalizzate: snap su elementi a larghezza fissa\n\nPer un carosello di card che deve fermarsi sempre allineato, `PageScrollPhysics` non basta (le card non occupano l'intero viewport). Ecco una physics di snap basata su un extent noto:\n\n```dart\nimport 'dart:ui' show clampDouble;\nimport 'package:flutter\u002Fphysics.dart';\nimport 'package:flutter\u002Fwidgets.dart';\n\nclass SnapScrollPhysics extends ScrollPhysics {\n  const SnapScrollPhysics({required this.itemExtent, super.parent});\n\n  final double itemExtent;\n\n  @override\n  SnapScrollPhysics applyTo(ScrollPhysics? ancestor) =>\n      SnapScrollPhysics(itemExtent: itemExtent, parent: buildParent(ancestor));\n\n  double _targetPixels(ScrollMetrics position, double velocity, Tolerance tolerance) {\n    var page = position.pixels \u002F itemExtent;\n    if (velocity \u003C -tolerance.velocity) {\n      page = page.floorToDouble();\n    } else if (velocity > tolerance.velocity) {\n      page = page.ceilToDouble();\n    } else {\n      page = page.roundToDouble();\n    }\n    return clampDouble(\n      page * itemExtent,\n      position.minScrollExtent,\n      position.maxScrollExtent,\n    );\n  }\n\n  @override\n  Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {\n    \u002F\u002F Fuori dai bordi lascia lavorare la physics genitore (overscroll\u002Frimbalzo).\n    if ((velocity \u003C= 0 && position.pixels \u003C= position.minScrollExtent) ||\n        (velocity >= 0 && position.pixels >= position.maxScrollExtent)) {\n      return super.createBallisticSimulation(position, velocity);\n    }\n\n    final tolerance = toleranceFor(position);\n    final target = _targetPixels(position, velocity, tolerance);\n    if ((target - position.pixels).abs() \u003C tolerance.distance) return null;\n\n    return ScrollSpringSimulation(\n      spring,\n      position.pixels,\n      target,\n      velocity,\n      tolerance: tolerance,\n    );\n  }\n\n  @override\n  bool get allowImplicitScrolling => false;\n}\n```\n\nUso:\n\n```dart\nListView.builder(\n  scrollDirection: Axis.horizontal,\n  physics: const SnapScrollPhysics(itemExtent: 280),\n  itemExtent: 280,\n  itemBuilder: (context, i) => PromoCard(index: i),\n)\n```\n\nDue dettagli importanti: `applyTo` deve restituire una nuova istanza con lo stesso stato (Flutter lo chiama per comporre le physics della piattaforma), e `toleranceFor(position)` sostituisce il vecchio getter `tolerance`, ormai deprecato.\n\nAltri override utili quando servono comportamenti più esotici: `applyPhysicsToUserOffset` (per ridurre la sensibilità del drag) e `applyBoundaryConditions` (per definire limiti o resistenze personalizzate).\n\n## ScrollBehavior: scroll con il mouse e scrollbar\n\nSu web e desktop il drag con il mouse è disabilitato di default. Per abilitarlo — ad esempio in un carosello che deve essere trascinabile anche con il cursore — si personalizza lo `ScrollBehavior`:\n\n```dart\nclass AppScrollBehavior extends MaterialScrollBehavior {\n  @override\n  Set\u003CPointerDeviceKind> get dragDevices => {\n        PointerDeviceKind.touch,\n        PointerDeviceKind.mouse,\n        PointerDeviceKind.trackpad,\n        PointerDeviceKind.stylus,\n      };\n}\n\nMaterialApp(\n  scrollBehavior: AppScrollBehavior(),\n  home: const HomePage(),\n);\n```\n\nPuoi applicarlo a un solo sottoalbero con `ScrollConfiguration(behavior: ..., child: ...)` e, sempre da lì, sovrascrivere `buildScrollbar` o `buildOverscrollIndicator` per rimuovere il glow di Android o forzare la scrollbar.\n\n## PrimaryScrollController e scroll annidati\n\nOgni `MaterialApp` mette a disposizione un `PrimaryScrollController`. È quello che permette alla `AppBar` di rispondere al tap sulla status bar in iOS (scroll-to-top) e che `NestedScrollView` usa per coordinare header e body. Se in una pagina hai più liste verticali, imposta `primary: false` su quelle secondarie per evitare che due `Scrollable` litighino sullo stesso controller.\n\nPer far scorrere la lista fino a un widget specifico, quando conosci il `BuildContext` dell'elemento:\n\n```dart\nScrollable.ensureVisible(\n  itemKey.currentContext!,\n  duration: const Duration(milliseconds: 300),\n  alignment: 0.1,\n);\n```\n\nFunziona però solo per gli elementi già costruiti: con `ListView.builder` e liste lunghe conviene calcolare l'offset a mano (se l'extent è fisso) oppure valutare un pacchetto come `scrollable_positioned_list`.\n\n## Preservare la posizione di scroll\n\nDue meccanismi spesso confusi:\n\n- **`PageStorageKey`**: salva l'offset quando il widget viene rimosso e ricostruito nella stessa sessione (tipico dei tab o delle pagine di una bottom navigation con `IndexedStack`).\n- **`restorationId`**: partecipa allo *state restoration* del sistema operativo, quindi sopravvive anche alla terminazione del processo.\n\n```dart\nListView.builder(\n  key: const PageStorageKey('tab_products'),\n  restorationId: 'tab_products',\n  itemBuilder: ...,\n)\n```\n\n## Checklist finale\n\n- Un `ScrollController` per ogni `Scrollable`, sempre con `dispose()`.\n- Nel listener del controller, aggiorna lo stato solo quando il valore *derivato* cambia.\n- Per reagire allo scroll senza pilotarlo, preferisci `NotificationListener` e controlla `notification.depth`.\n- Usa `ScrollEndNotification` + flag di guardia per la paginazione, non il listener continuo.\n- Estendi `ScrollPhysics` (con `applyTo` corretto) per snap e comportamenti custom invece di simulare l'aggancio con `animateTo` dopo il gesto.\n- Configura `ScrollBehavior` se l'app gira su web o desktop.\n- Aggiungi `PageStorageKey`\u002F`restorationId` dove la posizione conta per l'utente.\n\nControllare lo scroll in modo corretto è una di quelle cose che l'utente non nota mai — finché non funziona. Investire mezz'ora in queste API evita interfacce che \"saltano\", chiamate di rete duplicate e animazioni che scattano a metà.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fa3dd26ba-81c8-4b8b-9b72-8021fd84d73c.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1710993011776-b0cf571c7196?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAzOTUyOTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Solen Feyissa","https:\u002F\u002Funsplash.com\u002F@solenfeyissa","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-close-up-of-a-person-touching-a-cell-phone-rAiVi0DCL8g",null,"published","2026-09-26T04:01:37+00:00","Controllare lo scroll in Flutter: guida pratica","ScrollController, ScrollNotification e ScrollPhysics personalizzate in Flutter: snap, scroll-to-top, paginazione affidabile e scroll su desktop e web.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316909026]