restoration_kit 0.1.1
restoration_kit: ^0.1.1 copied to clipboard
Effortless state restoration: survive process death without RestorationMixin ceremony. One builder widget, drop-in controllers, and a debug guardian for Android's 1 MB limit.
restoration_kit #
Effortless state restoration for Flutter. Survive process death — no RestorationMixin ceremony, no restoreState overrides, no manual registration.
When Android or iOS kills your backgrounded app to reclaim memory, users come back to find their scroll position gone, their half-typed form erased, and their selected tab reset. Flutter ships a full framework to fix this, but the API is verbose enough that almost nobody uses it. This package removes the ceremony.
Before / after #
Vanilla Flutter — restoring one counter:
class _CounterState extends State<Counter> with RestorationMixin {
final RestorableInt _count = RestorableInt(0);
@override
String? get restorationId => 'counter';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_count, 'count');
}
@override
void dispose() {
_count.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) { /* ... */ }
}
With restoration_kit:
RestorableBuilder<int>(
restorationId: 'counter',
initialValue: 0,
builder: (context, count) => FilledButton(
onPressed: () => count.value++, // rebuilds AND persists
child: Text('${count.value}'),
),
)
Setup (one line) #
Restoration is off until you give your app a restoration scope:
MaterialApp(
restorationScopeId: 'app', // <-- this line
...
)
Forget it, and vanilla Flutter fails silently. This package instead throws a descriptive error in debug mode telling you exactly what to add.
Drop-in controllers #
Scroll offsets, text fields, and selected tabs are the state users most notice losing. One RestorationHost + drop-in controllers:
class _FeedScreenState extends State<FeedScreen> {
final scroll = RestoScrollController(restorationId: 'scroll');
final search = RestoTextController(restorationId: 'search');
@override
void dispose() {
scroll.dispose();
search.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RestorationHost(
restorationId: 'feed',
properties: [scroll, search], // registered, restored, disposed for you
child: Column(children: [
TextField(controller: search.controller),
Expanded(
child: ListView.builder(
controller: scroll.controller,
...
),
),
]),
);
}
}
RestoTabController works the same way (it additionally needs your State's TickerProviderStateMixin as vsync, because animations need one).
Ownership follows standard Flutter convention: you create the controllers, you dispose them in your State.dispose — exactly like a TextEditingController. The host only registers them. Keep the list stable (make them final fields, as above) and don't share one controller across two hosts.
The 1 MB guardian #
Android caps the entire restoration bundle at ~1 MB. Exceed it and your app dies in native code with a TransactionTooLargeException — no Dart stack trace, no hint of which widget did it.
restoration_kit tracks the serialized size of everything it persists and, in debug mode, prints a warning naming the offending restorationId long before you hit the cliff — including a per-property breakdown when the total approaches the limit. Compiled out of release builds entirely.
What restoration is (and isn't) for #
Restoration is for ephemeral UI state: scroll positions, unsubmitted form input, selected tabs, expanded/collapsed sections. It is not a database — it survives process death but not necessarily a user swiping the app away or a device restart. Anything that must truly persist belongs in real storage.
v1 type support #
RestorableBuilder<T> is deliberately strict: T must be serializable by Flutter's StandardMessageCodec — bool, int, double, String, or List/Map compositions of those. Unsupported types fail fast with a clear error instead of failing silently at restore time. Custom-type converters are on the roadmap.
Testing your app's restoration #
Flutter's test framework can simulate process death:
await tester.restartAndRestore();
See this package's own test suite for complete examples. To test on a real Android device: enable Don't keep activities in developer options, background your app, and reopen it.
API at a glance #
| Symbol | Purpose |
|---|---|
RestorableBuilder<T> |
Restore a single value with one widget |
RestorationHost |
Hosts any number of restorable properties, mixin-free |
RestoScrollController |
ScrollController that restores its offset |
RestoTextController |
TextEditingController that restores its text |
RestoTabController |
TabController that restores its index |
HostedRestorable |
Implement to plug your own restorables into a host |
Pure Dart. Zero dependencies beyond Flutter. Works on all six platforms.
Gotcha: restoration data lives with its widget #
A restorable's saved data is stored in a bucket owned by the widget that
registered it. When that widget's State is disposed, its bucket — and the
data in it — is deleted. The classic trap: a RestorableBuilder inside a
TabBarView or PageView loses its data every time its page goes
off-screen, because those widgets dispose off-screen children.
Fixes, pick one:
- Lift it — move the restorable above the
TabBarView/PageView. - Keep the page alive —
AutomaticKeepAliveClientMixinon the page. - Store at screen level — hold the state in a
RestorationHostthat never unmounts (this is why the drop-in controllers live on your State, not inside the page).
Related: swiping the app away from the recents screen is a user-initiated kill — the OS deliberately discards restoration data, by design, on both Android and iOS. Restoration only fires after system-initiated kills (memory pressure). To test the real thing:
# Background the app with the HOME button first (do not swipe it away), then:
adb shell am kill your.package.name
# Reopen from the launcher.
Or toggle Developer options → "Don't keep activities" while testing.