[{"data":1,"prerenderedAt":85},["ShallowReactive",2],{"tutorial-state-restoration-in-flutter-ripristinare-lo-stato-dellapp-con-restorationmixin":3,"comments-tutorial-state-restoration-in-flutter-ripristinare-lo-stato-dellapp-con-restorationmixin":84},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},109,"State restoration in Flutter: ripristinare lo stato dell'app con RestorationMixin","state-restoration-in-flutter-ripristinare-lo-stato-dellapp-con-restorationmixin","Impara a rendere la tua app Flutter resistente alla terminazione del processo: RestorationMixin, RestorableValue custom, rotte ripristinabili e test automatici con restartAndRestore.","Su Android (e in misura minore su iOS) il sistema operativo può **uccidere il processo** della tua app quando resta in background e la memoria scarseggia. Quando l'utente torna nell'app, Android ricrea l'Activity: l'utente si aspetta di ritrovare la stessa schermata, gli stessi campi compilati e la stessa posizione di scroll, ma un'app Flutter non preparata riparte dalla home con tutto lo stato azzerato.\n\nFlutter risolve questo problema con il **framework di state restoration**: un albero di `RestorationBucket` serializzati che il framework consegna al motore, il quale li passa alla piattaforma. Al riavvio i dati tornano indietro e i widget possono ricostruire il proprio stato prima del primo frame.\n\nIn questo tutorial avanzato vedremo come:\n\n- attivare la restoration con `restorationScopeId` e verificarla realmente sul dispositivo;\n- registrare proprietà con `RestorationMixin` e le `RestorableProperty` di serie;\n- rendere ripristinabili **rotte e dialoghi** con `Navigator.restorablePush` e `RestorableRouteFuture`;\n- scrivere una `RestorableValue` **custom** per modelli di dominio complessi;\n- coprire tutto con **widget test** usando `tester.restartAndRestore()`.\n\n> Prerequisiti: buona conoscenza di `StatefulWidget`, `Navigator` 2.0\u002F1.0 e dei widget test. Il codice è compatibile con Flutter 3.x.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fd2f2c737-f8e5-407b-b48f-e00f16941b70.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1705579607258-067cee27cf3a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA2NTYzNDF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"ThisisEngineering","https:\u002F\u002Funsplash.com\u002F@thisisengineering","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-cell-phone-sitting-on-top-of-a-table-h_X_uNGkwKo","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=Nh3ACJxBJIk",{"youtube_id":17,"duration_seconds":18,"published_at":19},"Nh3ACJxBJIk",156,"2026-09-29T11:01:34+00:00","advanced",45,"3.x","published","2026-09-29T04:32:21+00:00","State restoration in Flutter con RestorationMixin","Guida avanzata alla state restoration in Flutter: restorationScopeId, RestorationMixin, rotte ripristinabili, RestorableValue custom e test automatici.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,49,56,63,70,77],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},728,"Attivare la restoration e riprodurre la terminazione del processo","La state restoration è **opt-in**. Il primo passo è assegnare un `restorationScopeId` alla `MaterialApp` (o `CupertinoApp`): questo crea il bucket radice dal quale discendono tutti gli altri. Senza di esso, ogni `RestorationMixin` più in basso nell'albero resta inattivo e i dati non vengono mai serializzati.\n\nPer **testare davvero** il comportamento non basta mettere l'app in background: bisogna simulare la terminazione del processo da parte del sistema.\n\n**Android** (metodo consigliato):\n\n1. Impostazioni → Opzioni sviluppatore → attiva *Non mantenere le attività* (\"Don't keep activities\").\n2. Metti l'app in background (tasto Home) e riaprila dal task switcher.\n\nIn alternativa, da terminale con l'app in background:\n\n```bash\nadb shell am kill com.example.mia_app\n```\n\n**Attenzione:** usare `flutter run` in modalità debug e killare l'app dall'IDE **non** riproduce lo scenario, perché il processo viene distrutto insieme al bundle di restoration. Usa build `--profile` o `--release` installate sul dispositivo.\n\n**iOS:** la restoration è supportata; in simulatore puoi usare *Device → Trigger Memory Warning* oppure verificare via test automatici (passo 6), che restano il modo più affidabile e ripetibile.","import 'package:flutter\u002Fmaterial.dart';\n\nvoid main() => runApp(const MyApp());\n\nclass MyApp extends StatelessWidget {\n  const MyApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      \u002F\u002F Radice dell'albero di restoration: senza questo id nulla viene salvato.\n      restorationScopeId: 'app',\n      title: 'Restoration Demo',\n      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),\n      home: const HomePage(restorationId: 'home'),\n    );\n  }\n}","dart","L'app si avvia normalmente. Con \"Non mantenere le attività\" attivo, tornando nell'app il processo viene ricreato: al momento lo stato è ancora azzerato, ma l'infrastruttura di restoration è attiva.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":39,"expected_result":48,"demo_url":41,"video_url":41},729,2,"Registrare lo stato con RestorationMixin e le RestorableProperty","`RestorationMixin` si applica alla **classe `State`** di uno `StatefulWidget` e richiede due membri:\n\n- `restorationId`: identificatore univoco **tra i fratelli** nello stesso bucket. Se è `null` la restoration per quel sottoalbero è disabilitata.\n- `restoreState(oldBucket, initialRestore)`: qui si chiama `registerForRestoration` per ogni proprietà. Viene invocato **dopo `initState` e prima del primo `build`**, quindi lo stato è già ripristinato quando disegni la UI.\n\nRegole operative importanti:\n\n1. Le `RestorableProperty` vanno dichiarate come campi `final` e **rilasciate in `dispose()`**.\n2. Non leggere il valore prima di `registerForRestoration`: prima della registrazione le proprietà espongono il valore di default.\n3. `restoreState` può essere chiamato **più volte** (es. quando il bucket cambia): deve essere idempotente, quindi limitati a registrare proprietà.\n4. Le proprietà pronte all'uso includono `RestorableInt`, `RestorableDouble`, `RestorableBool`, `RestorableString`, `RestorableDateTime`, `RestorableEnum`, `RestorableTextEditingController` e le varianti `*N` nullable (`RestorableIntN`, ...).\n\nPer `TextField`, `ListView`\u002F`SingleChildScrollView` e `TabBarView` spesso basta passare direttamente un `restorationId` al widget: la posizione di scroll e il testo vengono gestiti dal framework.","class HomePage extends StatefulWidget {\n  const HomePage({super.key, this.restorationId});\n\n  final String? restorationId;\n\n  @override\n  State\u003CHomePage> createState() => _HomePageState();\n}\n\nclass _HomePageState extends State\u003CHomePage> with RestorationMixin {\n  final RestorableInt _counter = RestorableInt(0);\n  final RestorableBool _notifiche = RestorableBool(true);\n  final RestorableTextEditingController _noteController =\n      RestorableTextEditingController();\n\n  @override\n  String? get restorationId => widget.restorationId;\n\n  @override\n  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {\n    registerForRestoration(_counter, 'counter');\n    registerForRestoration(_notifiche, 'notifiche');\n    registerForRestoration(_noteController, 'note');\n  }\n\n  @override\n  void dispose() {\n    _counter.dispose();\n    _notifiche.dispose();\n    _noteController.dispose();\n    super.dispose();\n  }\n\n  void _incrementa() => setState(() => _counter.value++);\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Restoration Demo')),\n      body: ListView(\n        \u002F\u002F Anche la posizione di scroll viene ripristinata.\n        restorationId: 'home_list',\n        padding: const EdgeInsets.all(16),\n        children: [\n          Text('Contatore: ${_counter.value}',\n              style: Theme.of(context).textTheme.headlineSmall),\n          SwitchListTile(\n            title: const Text('Notifiche'),\n            value: _notifiche.value,\n            onChanged: (v) => setState(() => _notifiche.value = v),\n          ),\n          TextField(\n            controller: _noteController.value,\n            restorationId: 'note_field',\n            decoration: const InputDecoration(labelText: 'Note'),\n          ),\n          const SizedBox(height: 400),\n        ],\n      ),\n      floatingActionButton: FloatingActionButton(\n        onPressed: _incrementa,\n        child: const Icon(Icons.add),\n      ),\n    );\n  }\n}","Dopo aver incrementato il contatore, scritto una nota e scrollato, uccidendo il processo e riaprendo l'app ritrovi contatore, switch, testo e posizione di scroll esattamente come li avevi lasciati.",{"id":50,"position":51,"title":52,"body":53,"code_snippet":54,"code_language":39,"expected_result":55,"demo_url":41,"video_url":41},730,3,"Rendere ripristinabili rotte e dialoghi con restorablePush","Se l'utente è su una schermata di dettaglio o ha un dialog aperto quando il processo viene ucciso, la restoration delle sole proprietà non basta: bisogna ripristinare anche lo **stack di navigazione**.\n\nIl `Navigator` espone le varianti `restorablePush`, `restorablePushNamed`, `restorablePushReplacement`, ecc. La regola chiave è che il *route builder* deve essere una **funzione statica o top-level** (non una closure che cattura lo scope): il framework ne salva il riferimento e la richiama al riavvio. Gli `arguments` devono essere **primitivi serializzabili** dal `StandardMessageCodec` (numeri, stringhe, bool, liste e mappe di questi tipi).\n\nSe usi rotte con nome, assegna un `onGenerateRoute` e usa `restorablePushNamed`: gli argomenti seguono le stesse regole.\n\nQuando devi **recuperare il valore di ritorno** di una rotta ripristinata (tipico dei picker), usa `RestorableRouteFuture`: registrala come una qualunque `RestorableProperty` e implementa `onPresent` (come aprire la rotta) e `onComplete` (cosa fare con il risultato).","class _HomePageState extends State\u003CHomePage> with RestorationMixin {\n  final RestorableDateTime _dataSelezionata =\n      RestorableDateTime(DateTime(2024, 1, 1));\n\n  late final RestorableRouteFuture\u003CDateTime?> _datePickerRoute =\n      RestorableRouteFuture\u003CDateTime?>(\n    onComplete: _impostaData,\n    onPresent: (NavigatorState navigator, Object? arguments) {\n      return navigator.restorablePush(\n        _datePickerRouteBuilder,\n        arguments: _dataSelezionata.value.millisecondsSinceEpoch,\n      );\n    },\n  );\n\n  \u002F\u002F DEVE essere statica: il framework la richiama dopo il riavvio.\n  static Route\u003CDateTime> _datePickerRouteBuilder(\n    BuildContext context,\n    Object? arguments,\n  ) {\n    return DialogRoute\u003CDateTime>(\n      context: context,\n      builder: (context) => DatePickerDialog(\n        restorationId: 'date_picker_dialog',\n        initialDate: DateTime.fromMillisecondsSinceEpoch(arguments! as int),\n        firstDate: DateTime(2020),\n        lastDate: DateTime(2030),\n      ),\n    );\n  }\n\n  void _impostaData(DateTime? data) {\n    if (data == null) return;\n    setState(() => _dataSelezionata.value = data);\n  }\n\n  static Route\u003Cvoid> _dettaglioRouteBuilder(\n    BuildContext context,\n    Object? arguments,\n  ) {\n    return MaterialPageRoute\u003Cvoid>(\n      builder: (_) => DettaglioPage(id: arguments! as int),\n    );\n  }\n\n  void _apriDettaglio(int id) {\n    Navigator.of(context).restorablePush(_dettaglioRouteBuilder, arguments: id);\n  }\n\n  @override\n  String? get restorationId => widget.restorationId;\n\n  @override\n  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {\n    registerForRestoration(_dataSelezionata, 'data');\n    registerForRestoration(_datePickerRoute, 'date_picker_route');\n  }\n\n  @override\n  void dispose() {\n    _dataSelezionata.dispose();\n    _datePickerRoute.dispose();\n    super.dispose();\n  }\n\n  \u002F\u002F ... build()\n}","Aprendo la pagina di dettaglio o il date picker e uccidendo il processo, al ritorno nell'app la rotta (o il dialogo) è di nuovo aperta con gli stessi argomenti, e il valore scelto nel picker viene applicato correttamente.",{"id":57,"position":58,"title":59,"body":60,"code_snippet":61,"code_language":39,"expected_result":62,"demo_url":41,"video_url":41},731,4,"Creare una RestorableValue custom per i modelli di dominio","Le proprietà di serie coprono i tipi primitivi, ma nelle app reali si salva spesso un oggetto: un filtro di ricerca, una selezione multipla, lo step di un wizard.\n\nEstendendo `RestorableValue\u003CT>` devi implementare quattro membri:\n\n- `createDefaultValue()`: valore iniziale quando non c'è nulla da ripristinare;\n- `fromPrimitives(Object? data)`: deserializzazione dal bucket;\n- `toPrimitives()`: serializzazione (solo tipi supportati dallo `StandardMessageCodec`);\n- `didUpdateValue(T? oldValue)`: qui devi chiamare `notifyListeners()` quando il nuovo valore richiede un rebuild.\n\n**Vincoli da rispettare:**\n\n1. Mantieni il payload **piccolo** (indicativamente sotto qualche centinaio di KB): il bucket viaggia verso la piattaforma a ogni serializzazione. Non salvare liste di entità complete, salva **id** e ricarica i dati dalla cache\u002FDB.\n2. Non salvare **dati sensibili**: il bundle viene gestito dal sistema operativo, usa `flutter_secure_storage` per i token.\n3. Se il modello cambia tra due versioni dell'app, gestisci la **migrazione** in `fromPrimitives` con un campo `version` e un fallback al default.\n\nPer oggetti che sono già `Listenable` (es. un controller) esiste anche `RestorableListenable`; per contenitori più complessi puoi comporre più proprietà in un unico `RestorationMixin` dedicato.","import 'package:flutter\u002Fwidgets.dart';\n\nenum Ordinamento { recenti, popolari, prezzo }\n\n@immutable\nclass FiltroCatalogo {\n  const FiltroCatalogo({\n    this.query = '',\n    this.ordinamento = Ordinamento.recenti,\n    this.categorieIds = const \u003Cint>[],\n  });\n\n  final String query;\n  final Ordinamento ordinamento;\n  final List\u003Cint> categorieIds;\n\n  FiltroCatalogo copyWith({\n    String? query,\n    Ordinamento? ordinamento,\n    List\u003Cint>? categorieIds,\n  }) =>\n      FiltroCatalogo(\n        query: query ?? this.query,\n        ordinamento: ordinamento ?? this.ordinamento,\n        categorieIds: categorieIds ?? this.categorieIds,\n      );\n}\n\nclass RestorableFiltroCatalogo extends RestorableValue\u003CFiltroCatalogo> {\n  @override\n  FiltroCatalogo createDefaultValue() => const FiltroCatalogo();\n\n  @override\n  void didUpdateValue(FiltroCatalogo? oldValue) {\n    \u002F\u002F Il filtro è immutabile: qualunque cambio richiede un rebuild.\n    notifyListeners();\n  }\n\n  @override\n  FiltroCatalogo fromPrimitives(Object? data) {\n    final map = (data as Map?)?.cast\u003CString, Object?>();\n    if (map == null || map['v'] != 1) return createDefaultValue();\n    return FiltroCatalogo(\n      query: map['query'] as String? ?? '',\n      ordinamento: Ordinamento.values[(map['ordinamento'] as int?) ?? 0],\n      categorieIds:\n          (map['categorie'] as List?)?.cast\u003Cint>() ?? const \u003Cint>[],\n    );\n  }\n\n  @override\n  Object toPrimitives() => \u003CString, Object?>{\n        'v': 1,\n        'query': value.query,\n        'ordinamento': value.ordinamento.index,\n        \u002F\u002F Salviamo solo gli id, non le entità complete.\n        'categorie': value.categorieIds,\n      };\n}","Il filtro di ricerca viene serializzato come mappa di primitivi e ricostruito dopo il riavvio; se il formato salvato è di una versione precedente, si torna in sicurezza al filtro di default.",{"id":64,"position":65,"title":66,"body":67,"code_snippet":68,"code_language":39,"expected_result":69,"demo_url":41,"video_url":41},732,5,"Coordinare la restoration con lo state management e i dati remoti","La restoration **non è persistenza**: il bundle sopravvive alla terminazione del processo ma non alla chiusura esplicita dell'app da parte dell'utente né a un reboot. Il pattern corretto è quindi:\n\n- **restoration** → stato *effimero della UI*: tab attiva, scroll, testo in bozza, filtri, rotta corrente, id dell'elemento aperto;\n- **storage (Hive\u002FSQLite\u002Fsecure storage)** → stato *di dominio*: sessione utente, dati scaricati, preferenze.\n\nSe usi Provider\u002FRiverpod\u002FBLoC, non tentare di serializzare il bloc intero: registra le **chiavi minime** nel `RestorationMixin` e usale per riallineare lo state manager subito dopo il ripristino. Il momento giusto è dopo `restoreState`, in `initState`\u002F`didChangeDependencies` con un `addPostFrameCallback`, così eviti di leggere il valore prima della registrazione.\n\nUn pattern robusto: la pagina di dettaglio riceve solo l'`id` come argomento ripristinabile e ricarica i dati dal repository (che a sua volta legge dalla cache offline). Così il payload di restoration resta minuscolo e i dati mostrati sono sempre freschi.","class _CatalogoPageState extends State\u003CCatalogoPage> with RestorationMixin {\n  final RestorableFiltroCatalogo _filtro = RestorableFiltroCatalogo();\n  final RestorableIntN _dettaglioApertoId = RestorableIntN(null);\n\n  @override\n  String? get restorationId => 'catalogo';\n\n  @override\n  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {\n    registerForRestoration(_filtro, 'filtro');\n    registerForRestoration(_dettaglioApertoId, 'dettaglio_id');\n\n    if (initialRestore) {\n      \u002F\u002F Riallinea lo state manager DOPO il primo frame:\n      \u002F\u002F qui il valore ripristinato è già disponibile.\n      WidgetsBinding.instance.addPostFrameCallback((_) {\n        if (!mounted) return;\n        context.read\u003CCatalogoCubit>().applicaFiltro(_filtro.value);\n      });\n    }\n  }\n\n  void _onFiltroCambiato(FiltroCatalogo nuovo) {\n    setState(() => _filtro.value = nuovo);\n    context.read\u003CCatalogoCubit>().applicaFiltro(nuovo);\n  }\n\n  @override\n  void dispose() {\n    _filtro.dispose();\n    _dettaglioApertoId.dispose();\n    super.dispose();\n  }\n\n  \u002F\u002F ... build()\n}","Dopo il ripristino, la lista viene ricaricata dal repository applicando automaticamente il filtro salvato, senza che l'utente debba reimpostarlo.",{"id":71,"position":72,"title":73,"body":74,"code_snippet":75,"code_language":39,"expected_result":76,"demo_url":41,"video_url":41},733,6,"Testare la restoration in modo automatico con restartAndRestore","Verificare a mano con \"Non mantenere le attività\" è utile una volta; in CI serve un test deterministico. `WidgetTester` offre tre API dedicate:\n\n- `await tester.restartAndRestore()`: distrugge e ricostruisce l'albero usando i dati di restoration correnti, esattamente come farebbe il sistema;\n- `final data = await tester.getRestorationData()`: cattura uno snapshot del bundle;\n- `await tester.restoreFrom(data)`: ripristina l'app da uno snapshot precedente (utile per simulare \"kill dopo N secondi\" e ignorare le interazioni successive).\n\nRicorda che nei test l'`restorationScopeId` deve essere presente: pompa direttamente la tua `MaterialApp` reale, non un widget isolato senza scope.\n\nInserisci questi test nella pipeline CI insieme ai golden test: una regressione sulla restoration è silenziosa e difficilissima da notare durante lo sviluppo quotidiano.","import 'package:flutter\u002Fmaterial.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:mia_app\u002Fmain.dart';\n\nvoid main() {\n  testWidgets('il contatore e la nota sopravvivono al riavvio', (tester) async {\n    await tester.pumpWidget(const MyApp());\n\n    await tester.tap(find.byType(FloatingActionButton));\n    await tester.tap(find.byType(FloatingActionButton));\n    await tester.enterText(find.byType(TextField), 'bozza importante');\n    await tester.pumpAndSettle();\n\n    expect(find.text('Contatore: 2'), findsOneWidget);\n\n    \u002F\u002F Simula il kill del processo e il successivo ripristino.\n    await tester.restartAndRestore();\n\n    expect(find.text('Contatore: 2'), findsOneWidget);\n    expect(find.text('bozza importante'), findsOneWidget);\n  });\n\n  testWidgets('la rotta di dettaglio viene ripristinata', (tester) async {\n    await tester.pumpWidget(const MyApp());\n\n    await tester.tap(find.text('Apri dettaglio'));\n    await tester.pumpAndSettle();\n    expect(find.byType(DettaglioPage), findsOneWidget);\n\n    \u002F\u002F Snapshot del bundle: tutto ciò che accade dopo verrà scartato.\n    final TestRestorationData data = await tester.getRestorationData();\n\n    await tester.tap(find.byTooltip('Back'));\n    await tester.pumpAndSettle();\n    expect(find.byType(DettaglioPage), findsNothing);\n\n    await tester.restoreFrom(data);\n    expect(find.byType(DettaglioPage), findsOneWidget);\n  });\n}","Entrambi i test passano: lo stato dei widget e lo stack di navigazione vengono ricostruiti dopo `restartAndRestore()` e `restoreFrom()`, dando una rete di sicurezza in CI contro le regressioni.",{"id":78,"position":79,"title":80,"body":81,"code_snippet":82,"code_language":39,"expected_result":83,"demo_url":41,"video_url":41},734,7,"Checklist finale e insidie da evitare","Prima di considerare la feature completa, verifica questi punti:\n\n1. **`restorationScopeId` ovunque serve.** Se una sotto-parte dell'app usa un `Navigator` annidato (es. tab con stack indipendenti), assegna un `restorationScopeId` anche a quel `Navigator`, altrimenti le sue rotte non verranno salvate.\n2. **Id univoci tra fratelli.** Due `RestorationMixin` fratelli con lo stesso `restorationId` provocano un'eccezione in debug: usa prefissi legati alla feature (`catalogo_lista`, `catalogo_filtri`).\n3. **Liste dinamiche.** Per elementi generati in un ciclo, deriva l'id dalla chiave stabile dell'elemento (`'item_${item.id}'`), mai dall'indice.\n4. **Niente oggetti non serializzabili.** `toPrimitives` accetta solo ciò che supporta lo `StandardMessageCodec`: niente `DateTime` diretti (usa `millisecondsSinceEpoch`), niente enum (usa `index` o il nome), niente istanze di classi.\n5. **Closure nelle rotte ripristinabili.** `restorablePush` con un builder non statico lancia un assert: estrai sempre una funzione top-level o `static`.\n6. **Payload sotto controllo.** Monitora la dimensione del bundle: se cresce, stai salvando dati di dominio che dovrebbero stare in un database locale.\n7. **Privacy.** Nessun token, password o dato personale nel bucket.\n8. **Web e desktop.** Il framework funziona, ma sulle piattaforme dove il processo non viene ucciso dal sistema il beneficio è limitato: concentra lo sforzo su Android.\n\nUn ultimo consiglio: introduci la restoration **per feature**, partendo dai flussi lunghi (form multi-step, checkout, editor). Sono quelli dove la perdita di stato costa di più all'utente e dove l'investimento si ripaga subito.","\u002F\u002F Esempio: Navigator annidato dentro una tab, con il proprio scope.\nNavigator(\n  key: _tabNavigatorKey,\n  restorationScopeId: 'tab_catalogo_navigator',\n  onGenerateRoute: (settings) => MaterialPageRoute\u003Cvoid>(\n    settings: settings,\n    builder: (_) => const CatalogoPage(),\n  ),\n);\n\n\u002F\u002F Esempio: id derivato da una chiave stabile, mai dall'indice.\nListView.builder(\n  restorationId: 'catalogo_lista',\n  itemCount: prodotti.length,\n  itemBuilder: (context, index) {\n    final prodotto = prodotti[index];\n    return ProdottoTile(\n      key: ValueKey(prodotto.id),\n      restorationId: 'prodotto_${prodotto.id}',\n      prodotto: prodotto,\n    );\n  },\n);","L'app supera il test del \"Don't keep activities\" su tutti i flussi critici: l'utente non perde mai il lavoro in corso e la CI protegge il comportamento nel tempo.",[],1792316928558]