Make Your Flutter App Survive Process Death: A Practical Guide to State Restoration
When Android kills a backgrounded Flutter app, users lose their place. This post shows how to use RestorationMixin and RestorableProperty types to keep forms, navigation, and scroll positions intact across process death.
05 Feb 2026, 11:52 UTC

Why State Restoration Matters
On Android and iOS, the operating system can kill a backgrounded Flutter app to reclaim memory. When the user brings the app back, the framework starts a fresh process and shows the initial route. Any form input, scroll position, or deep navigation state is lost. For most apps this feels like a cold restart, breaking the user experience. Flutter’s RestorationMixin and RestorableProperty classes give you a way to persist small, serializable pieces of state so that the user returns exactly where they left off.
How the Restoration Framework Works
The core of the framework is a RestorationBucket, a key/value store that lives in the OS. Widgets that want to be restored mix in RestorationMixin and register RestorableProperty instances in restoreState(). Each property must have a unique restorationId that the framework uses to look up the saved value when the widget tree is rebuilt after a process kill.
For the framework to route data back to the right widget, the widget tree must be wrapped in a RestorationScope. The easiest way is to set MaterialApp.restorationScopeId to a stable string; this automatically creates a root RestorationScope for the entire app.
Setting Up a Simple Restorable Form
Below is a minimal example of a settings screen that preserves a text field and a toggle across process death. The code is intended for Flutter 3.10+.
class SettingsPage extends StatefulWidget {
const SettingsPage({Key? key}) : super(key: key);
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> with RestorationMixin {
// Unique id for the restoration bucket of this page
@override
String get restorationId => 'settings_page';
// Restorable properties
final _username = RestorableTextEditingController();
final _darkMode = RestorableBool(false);
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// Register each property with a unique restorationId
registerForRestoration(_username, 'username_controller');
registerForRestoration(_darkMode, 'dark_mode_toggle');
}
@override
void dispose() {
_username.dispose();
_darkMode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _username.value,
decoration: const InputDecoration(labelText: 'Username'),
),
SwitchListTile(
title: const Text('Dark mode'),
value: _darkMode.value,
onChanged: (v) => setState(() => _darkMode.value = v),
),
],
),
),
);
}
}
Key points:
restorationIdon theStatemust be stable across rebuilds and app releases.- Each
RestorablePropertygets its ownrestorationIdwhen registered. - Dispose the properties in
dispose()to free resources. - Only small, serializable data should be stored. Large lists, network streams, or complex objects should be re‑fetched after restoration, using the saved value as a key.
Testing the Restoration Flow
To confirm that restoration works, enable “Don’t keep activities” on Android (Settings → Developer options). Run the app, navigate to SettingsPage, enter a username and toggle dark mode, then press the home button. Reopen the app; the text field and switch should retain their values. On iOS, you can simulate app termination in Xcode by stopping the process and relaunching.
During a hot reload you will not see restoration because the process remains alive. Always test with a real process kill.
Trade‑offs & Limitations
- Scope of Restorable Properties: Only
Stateobjects that mix inRestorationMixincan register properties. If you use global state managers (Bloc, Riverpod, etc.) you must bridge them to the restoration bucket manually. - Type Stability: Changing the type of a
RestorableProperty(e.g., fromRestorableBooltoRestorableInt) will cause deserialization errors for users upgrading the app. Keep IDs and types stable or provide a migration strategy. - Custom Types: For user‑defined types you need to implement
RestorablePropertyyourself, providingfromPrimitivesandtoPrimitivesmethods. Mistakes here can silently corrupt state. - Large Data: Avoid storing large data structures; the restoration bucket is meant for lightweight state. Use the saved value as a key to reload data from a cache or network.
Actionable Next Steps
- Wrap your
MaterialAppwithrestorationScopeId: 'app'to create a root restoration scope. - Identify the screens that lose critical state on kill (e.g., forms, lists, navigation stacks).
- For each screen, add
RestorationMixinand register the necessaryRestorablePropertyinstances. - Test with “Don’t keep activities” and real app termination to verify that state is restored.
- Document any custom restoration logic and keep your property types stable across releases.
By following these steps you can give users a seamless experience, even when the OS forces your Flutter app to restart.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.