[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-state-restoration-in-flutter-salvare-la-ui-quando-android-uccide-lapp":3,"comments-article-state-restoration-in-flutter-salvare-la-ui-quando-android-uccide-lapp":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},142,"State restoration in Flutter: salvare la UI quando Android uccide l'app","state-restoration-in-flutter-salvare-la-ui-quando-android-uccide-lapp","Quando il sistema termina il processo in background, l'utente che torna nell'app trova tutto azzerato. Scopriamo RestorationMixin, le Restorable properties e il restore della navigazione per ripristinare la UI esattamente com'era.","## Il problema che quasi nessuno testa\n\nUn utente sta compilando un form nella tua app, riceve una chiamata, apre la fotocamera, risponde a un messaggio. Dieci minuti dopo torna nella tua app toccando l'icona nel multitasking e... si ritrova sulla home, con il form vuoto.\n\nNon è un bug del tuo codice: Android (e in misura minore iOS) termina i processi in background quando ha bisogno di memoria. Quando l'utente rientra, il sistema **ricrea l'app da zero** ma si aspetta che la UI appaia esattamente come l'aveva lasciata. Questo meccanismo si chiama *state restoration* e Flutter lo supporta nativamente dalla versione 1.22, anche se resta una delle funzionalità più ignorate del framework.\n\nAttenzione: la state restoration **non è** persistenza dei dati. I dati di dominio (prodotti, messaggi, profili) vanno su un database locale. Qui parliamo di stato effimero della UI: la tab selezionata, la posizione di scroll, il testo digitato, lo step di un wizard, la rotta corrente.\n\n## Come riprodurre il problema in 30 secondi\n\nPrima di scrivere codice, impara a testarlo. Su Android:\n\n1. Opzioni sviluppatore → **Non mantenere le attività** (\"Don't keep activities\") → ON\n2. Apri la tua app, naviga in profondità, metti dell'input\n3. Vai in home, apri un'altra app, torna indietro\n\nSe l'app riparte dalla schermata iniziale, non hai la restoration attiva. In alternativa, da terminale:\n\n```bash\nadb shell am kill it.tuodominio.tuaapp\n```\n\nSu iOS il sistema è più conservativo, ma lo stesso meccanismo entra in gioco quando il sistema rimuove l'app dalla memoria (non quando l'utente la chiude manualmente con lo swipe: in quel caso lo stato viene volutamente scartato su entrambe le piattaforme).\n\n## Attivare la restoration: una riga\n\nTutto parte dal `restorationScopeId` sulla root dell'app. Senza questo, nessun meccanismo di restore è attivo.\n\n```dart\nMaterialApp(\n  restorationScopeId: 'app',\n  home: const HomePage(),\n);\n```\n\nImpostandolo ottieni subito due vantaggi gratuiti:\n\n- il `Navigator` radice ripristina lo stack delle rotte dichiarate (con le dovute accortezze, vedi sotto);\n- i widget che espongono un `restorationId` iniziano a funzionare.\n\nMolti widget del framework accettano già un `restorationId`:\n\n```dart\nListView.builder(\n  restorationId: 'lista_prodotti', \u002F\u002F ripristina l'offset di scroll\n  itemCount: prodotti.length,\n  itemBuilder: (context, i) => ProductTile(prodotti[i]),\n);\n\nTextField(\n  restorationId: 'campo_ricerca', \u002F\u002F ripristina testo e selezione\n  controller: _controller,\n);\n```\n\nFunziona anche con `PageView`, `TabBarView`, `SingleChildScrollView`, `CupertinoTabScaffold` e simili. È il frutto migliore a portata di mano: aggiungere un `restorationId` alle liste principali costa pochi minuti.\n\n## RestorationMixin: ripristinare lo stato dei tuoi widget\n\nPer lo stato custom dentro uno `StatefulWidget` serve il `RestorationMixin`. Il pattern è sempre lo stesso: sostituisci i campi normali con *Restorable properties* e le registri in `restoreState`.\n\n```dart\nclass CheckoutPage extends StatefulWidget {\n  const CheckoutPage({super.key});\n\n  @override\n  State\u003CCheckoutPage> createState() => _CheckoutPageState();\n}\n\nclass _CheckoutPageState extends State\u003CCheckoutPage> with RestorationMixin {\n  \u002F\u002F Al posto di: int _step = 0;\n  final RestorableInt _step = RestorableInt(0);\n  final RestorableBool _accettaTermini = RestorableBool(false);\n  final RestorableTextEditingController _note = RestorableTextEditingController();\n\n  @override\n  String? get restorationId => 'checkout_page';\n\n  @override\n  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {\n    registerForRestoration(_step, 'step');\n    registerForRestoration(_accettaTermini, 'termini');\n    registerForRestoration(_note, 'note');\n  }\n\n  @override\n  void dispose() {\n    _step.dispose();\n    _accettaTermini.dispose();\n    _note.dispose();\n    super.dispose();\n  }\n\n  void _avanti() => setState(() => _step.value++);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text('Step ${_step.value + 1} di 3')),\n      body: Column(\n        children: [\n          TextField(controller: _note.value),\n          CheckboxListTile(\n            value: _accettaTermini.value,\n            onChanged: (v) => setState(() => _accettaTermini.value = v ?? false),\n            title: const Text('Accetto i termini'),\n          ),\n          FilledButton(onPressed: _avanti, child: const Text('Avanti')),\n        ],\n      ),\n    );\n  }\n}\n```\n\nPunti chiave da ricordare:\n\n- `restorationId` deve essere **univoco nel suo scope**: se la stessa pagina compare più volte (per esempio in tab diverse) usa id differenti o avvolgi in un `RestorationScope`.\n- le Restorable properties vanno sempre **dispose()**: sono `ChangeNotifier`.\n- `restoreState` viene chiamato anche al primo avvio, con `initialRestore == true`. Serve per inizializzare, non solo per ripristinare.\n- modificare `.value` notifica automaticamente il framework che lo stato va riserializzato, ma **non ricostruisce** il widget: il `setState` serve ancora.\n\nLe classi pronte all'uso sono numerose: `RestorableInt`, `RestorableDouble`, `RestorableBool`, `RestorableString`, `RestorableNum`, `RestorableDateTime`, `RestorableEnum\u003CT>`, i loro equivalenti nullable (`RestorableIntN`, `RestorableStringN`...), `RestorableTextEditingController` e `RestorableChangeNotifier`.\n\n## Restorable personalizzate per i tuoi modelli\n\nSe devi ripristinare un oggetto tuo, estendi `RestorableValue\u003CT>`. I dati devono essere serializzabili dallo `StandardMessageCodec` (numeri, stringhe, booleani, liste e mappe di questi tipi).\n\n```dart\nclass Filtro {\n  const Filtro({required this.categoria, required this.prezzoMax});\n  final String categoria;\n  final double prezzoMax;\n}\n\nclass RestorableFiltro extends RestorableValue\u003CFiltro> {\n  RestorableFiltro(this._default);\n  final Filtro _default;\n\n  @override\n  Filtro createDefaultValue() => _default;\n\n  @override\n  void didUpdateValue(Filtro? oldValue) {\n    \u002F\u002F Avvisa il framework che lo stato serializzato è cambiato\n    notifyListeners();\n  }\n\n  @override\n  Filtro fromPrimitives(Object? data) {\n    final map = data! as Map\u003CObject?, Object?>;\n    return Filtro(\n      categoria: map['categoria']! as String,\n      prezzoMax: map['prezzoMax']! as double,\n    );\n  }\n\n  @override\n  Object toPrimitives() => \u003CString, Object>{\n        'categoria': value.categoria,\n        'prezzoMax': value.prezzoMax,\n      };\n}\n```\n\n## Ripristinare la navigazione\n\nSe l'utente era tre schermate in profondità, riportarlo alla home è comunque una sconfitta. Con il `Navigator` imperativo si usano le varianti `restorable*`, che richiedono un builder **statico o top-level** (non può essere una closure, perché deve essere ricostruibile dopo il riavvio del processo):\n\n```dart\nclass _HomeState extends State\u003CHome> with RestorationMixin {\n  @override\n  String? get restorationId => 'home';\n\n  @override\n  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {}\n\n  static Route\u003Cvoid> _dettaglioRoute(BuildContext context, Object? arguments) {\n    return MaterialPageRoute\u003Cvoid>(\n      builder: (_) => DettaglioPage(id: arguments! as String),\n    );\n  }\n\n  void _apriDettaglio(String id) {\n    Navigator.of(context).restorablePush(_dettaglioRoute, arguments: id);\n  }\n}\n```\n\nEsistono anche `restorablePushNamed`, `restorablePushReplacement` e `RestorableRouteFuture` per gestire il valore di ritorno di una rotta (tipico dei dialog che restituiscono una scelta).\n\nSe usi il routing dichiarativo con **go_router**, il discorso si semplifica: basta passare un `restorationScopeId` al router e usare pagine con `restorationId`.\n\n```dart\nfinal router = GoRouter(\n  restorationScopeId: 'router',\n  routes: [\n    GoRoute(\n      path: '\u002Fdettaglio\u002F:id',\n      pageBuilder: (context, state) => MaterialPage(\n        restorationId: 'dettaglio_${state.pathParameters['id']}',\n        child: DettaglioPage(id: state.pathParameters['id']!),\n      ),\n    ),\n  ],\n);\n\nMaterialApp.router(\n  restorationScopeId: 'app',\n  routerConfig: router,\n);\n```\n\n## Testare la restoration nei widget test\n\nLa parte più sottovalutata: puoi verificare il comportamento senza toccare un dispositivo reale. `WidgetTester` espone metodi dedicati.\n\n```dart\ntestWidgets('lo step del checkout sopravvive al restore', (tester) async {\n  await tester.pumpWidget(const MyApp());\n\n  await tester.tap(find.text('Avanti'));\n  await tester.pumpAndSettle();\n  expect(find.text('Step 2 di 3'), findsOneWidget);\n\n  \u002F\u002F Simula la distruzione e ricreazione del processo\n  await tester.restartAndRestore();\n\n  expect(find.text('Step 2 di 3'), findsOneWidget);\n});\n```\n\nPer scenari più complessi puoi catturare i dati e riapplicarli manualmente:\n\n```dart\nfinal data = await tester.getRestorationData();\nawait tester.restartAndRestore();\nawait tester.restoreFrom(data);\n```\n\n## Limiti e best practice\n\n- **Tieni i payload piccoli.** Su Android lo stato finisce nel `Bundle` dell'Activity: superare il limite della transazione Binder (circa 1 MB complessivo) provoca una `TransactionTooLargeException`. Mai salvare liste di risultati, immagini in base64 o risposte JSON intere: salva gli **identificativi** e ricarica i dati dal repository.\n- **Separa i ruoli.** Lo stato di dominio va in database o cache (Drift, Isar, Hive); la restoration serve solo a ricostruire il contesto visivo.\n- **Non salvare dati sensibili.** Il bundle di sistema non è uno storage cifrato: token e credenziali restano in `flutter_secure_storage`.\n- **Attenzione ai gestori di stato globali.** Provider, Riverpod o BLoC vivono fuori dall'albero dei widget: dopo il kill del processo ripartono vuoti. Puoi combinare le due cose registrando nel bucket di restoration i parametri minimi (id selezionato, filtro attivo) e lasciando che il provider ricarichi il resto all'avvio.\n- **Procedi per priorità.** Non serve restaurare tutto: concentrati su form lunghi, wizard multi-step, flussi di checkout, liste con scroll profondo e sul percorso di navigazione.\n\n## Conclusione\n\nLa state restoration è uno di quei dettagli che l'utente non nota mai quando funziona e che genera recensioni negative quando manca. Il costo di ingresso è bassissimo: `restorationScopeId` sulla `MaterialApp`, qualche `restorationId` sulle liste e sui campi di testo, e il `RestorationMixin` sulle due o tre schermate dove perdere il lavoro dell'utente fa più male. Attiva \"Non mantenere le attività\" sul tuo device di sviluppo e lascialo acceso: scoprirai quanti flussi della tua app non sopravvivono a una semplice telefonata.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F00db0c81-4b7b-4548-87c2-d91dc4382b43.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1769596722452-d88328e1a19e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTMyNDY0OTZ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Swello","https:\u002F\u002Funsplash.com\u002F@getswello","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-smartphone-with-many-app-icons-displayed-mJxQXpkmZx4",null,"published","2026-10-29T04:01:36+00:00","State restoration in Flutter con RestorationMixin","Guida pratica alla state restoration in Flutter: restorationScopeId, RestorationMixin, Restorable properties, restore della navigazione e test automatici.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1793274033557]