Implementing State Restoration in Flutter: A Practical Guide
Learn how to make a Flutter screen survive process death on Android and iOS. The guide walks through enabling restoration, using RestorableInt and RestorableTextEditingController, registering properties, and testing with real device scenarios.
10 Apr 2026, 14:53 UTC

Desired Outcome
Make a Flutter screen retain its UI state—such as a counter value, text input, and tab selection—across process death on Android and iOS. After the operating system kills the app to reclaim memory, the user returns to the same visual state as if the app had never closed.
Prerequisites
- Flutter SDK 3.10+ (or any version that includes the RestorationMixin API). Verify with
flutter --version. - App built with
MaterialApp(orCupertinoApp) as the root widget. - Basic familiarity with Flutter stateful widgets, controller classes, and the build‑time lifecycle.
- Android Studio or Xcode for emulators/simulators to trigger process death.
Procedure
- Enable the restoration scope on the app.
Wrap your root widget with
MaterialAppand supply a non‑nullrestorationScopeId. This is the global key the OS uses to store and retrieve state.MaterialApp( restorationScopeId: 'app', // <-- required home: const MyHomePage(), ); - Make the stateful widget opt‑in.
Extend
State<MyHomePage>withRestorationMixinand overriderestorationId.class _MyHomePageState extends State<MyHomePage> with RestorationMixin { @override String get restorationId => 'home_page'; // … - Replace plain fields with restorable properties.
For each piece of state you want to survive, use a
Restorable*class. Below we keep a counter, a text field, and a selected tab index.final RestorableInt _counter = RestorableInt(0); final RestorableTextEditingController _controller = RestorableTextEditingController(); final RestorableInt _selectedTab = RestorableInt(0); - Register the properties in
restoreState.Call
registerForRestorationfor each property, providing a uniquerestorationId. The IDs must stay the same across releases.@override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_counter, 'counter'); registerForRestoration(_controller, 'text_controller'); registerForRestoration(_selectedTab, 'tab_index'); } - Use the restorable values in the UI.
Read from the property’s
valuefield and write back when the UI changes.@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Restoration Demo')), body: Column( children: [ Text('Counter: ${_counter.value}'), ElevatedButton( onPressed: () => setState(() => _counter.value++), child: const Text('Increment'), ), TextField(controller: _controller.value), Expanded( child: TabBarView( controller: TabController( length: 2, vsync: this, initialIndex: _selectedTab.value, ), children: const [ Center(child: Text('Tab 1')), Center(child: Text('Tab 2')), ], ), ), ], ), ); } - Dispose the controllers.
Restorable controllers need explicit disposal.
RestorationMixindoes not calldisposeon them.@override void dispose() { _controller.dispose(); super.dispose(); } - Test process‑death restoration.
On Android:
- Enable Developer Options →
Don't keep activities. - Run the app, interact to change state.
- Press the back button or switch to another app.
- Return to the app; the UI should restore to the state before the activity was destroyed.
- Enable Developer Options →
- Use a physical device or simulator with the
Debug → Simulate Background Fetchoption to trigger termination. - Re‑launch the app; the last known state should appear.
Expected Checks
- restoreState runs. Add a debug print inside
restoreStateto confirm it executes after a process kill. - IDs match. If you rename a
restorationId, the app will ignore the stored data and start fresh; the debug print will show a missing bucket. - Data size. Keep restorable values small (int, String, bool). Large objects will cause serialization errors.
- No sensitive data. Avoid storing passwords or tokens in restorable fields; the OS may expose them in backups.
Recovery Options
- Graceful fallback. If restoration fails (e.g., due to corrupted data), the framework automatically falls back to the widget’s default constructor values. Ensure defaults are acceptable.
- Manual reset. Expose a button that clears restoration data by calling
restorationBucket?.removeAll();and thensetState(() {});. - Version migration. When changing the data model, create a custom
RestorablePropertythat can read legacy primitives and map them to the new structure.
Limitations & Practical Verification
Restoration works only when the OS preserves the process data. On iOS, the system may purge the data after a reboot or if the user deletes the app. Android may discard the state if the app is not in the recent tasks list. Always test on real devices for production confidence.
When the app is terminated normally (user swipes away from recent tasks), the restoration data is cleared. To verify that your implementation is robust, trigger a kill -9 on the process via Android Studio’s adb shell kill command and confirm the UI state persists after the next launch.
Conclusion
By following the steps above—enabling the restoration scope, opting in with RestorationMixin, replacing mutable fields with Restorable* classes, registering them, and testing with process death—you can deliver a seamless user experience across app restarts. The approach is declarative, requires minimal boilerplate, and integrates cleanly with Flutter’s existing state management patterns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.