Implementing State Restoration
1. Understanding State Restoration Concept
State restoration in Flutter allows an app to preserve and restore the state of its UI when the app is closed and reopened. This ensures a seamless user experience by maintaining the app's state across sessions.
| Concept | Description | Details |
|---|---|---|
| State Restoration | Process of saving and restoring UI state | Enables apps to maintain user context across app sessions |
| Automatic Restoration | Flutter automatically restores basic UI state | Includes scroll positions, text field values, and toggle states |
| Manual Restoration | Developer-controlled state restoration | Used for complex state like navigation stacks or custom data models |
Example: Enabling State Restoration
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
restorationScopeId: 'my_app',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with RestorationMixin {
String? _restoredValue;
@override
void restoreState(RestorationBucket? bucket, bool isReset) {
_restoredValue = bucket?.read<String>('value');
}
@override
void saveState(RestorationBucket bucket) {
bucket.write('value', _restoredValue);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('State Restoration')),
body: Center(
child: Text(_restoredValue ?? 'No value restored'),
),
);
}
}
restorationScopeId is crucial for grouping related state. It must be unique within the app to prevent conflicts.
2. Enabling State Restoration (restorationScopeId)
| Property | Description | Values |
|---|---|---|
| restorationScopeId | Identifier for the state restoration scope | String |
Example: Enabling State Restoration
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> with RestorationMixin {
final String restorationId = 'my_widget_restoration_id';
@override
void restoreState(Snapshot? snapshot, restorationId) {
// Restore state logic here
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('State restored!'),
),
),
);
}
}
3. Using RestorationMixin
The RestorationMixin is a powerful tool in Flutter that simplifies state restoration by automatically managing the lifecycle of restoration data for widgets.
| Property | Description | Usage |
|---|---|---|
| restorationId | Unique identifier for the widget's restoration state. | String or RestorationId |
| restoredState | Callback to handle restored state values. | void Function(RestoredState) |
Example: Using RestorationMixin
class MyWidget extends StatefulWidget with RestorationMixin {
@override
final String restorationId = 'my_widget';
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with RestorationMixin {
int counter = 0;
@override
void restoreState(RestoredState? restored, bool isInitialRestore) {
if (restored != null) {
counter = restored.getInt('counter') ?? 0;
}
}
void incrementCounter() {
setState(() {
counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Restored Counter')),
body: Center(
child: Text('Counter: $counter'),
),
floatingActionButton: FloatingActionButton(
onPressed: incrementCounter,
child: Icon(Icons.add),
),
);
}
}
restorationId must be unique within the scope of the widget tree to avoid conflicts in state restoration.
4. Creating RestorableProperties
Restorable properties in Flutter allow you to save and restore the state of widgets across app sessions. They are particularly useful for maintaining user input, selections, and other transient data.
| Property | Type | Description | Example |
|---|---|---|---|
| RestorableProperty<T> | Generic | Base class for restorable properties. Subclasses define specific types like RestorableString, RestorableInt. |
final _counter = RestorableInt(0); |
| RestorableString | String | Used to restore string values like user input or text fields. | final _textFieldValue = RestorableString(''); |
| RestorableInt | Integer | Used to restore integer values like counters or indices. | final _selectedIndex = RestorableInt(0); |
| RestorableDouble | Double | Used to restore double values like slider positions or decimal inputs. | final _sliderValue = RestorableDouble(0.5); |
| RestorableBool | Boolean | Used to restore boolean values like toggle switches or flags. | final _isSwitchOn = RestorableBool(false); |
Example: Using RestorableProperty in a StatefulWidget
class MyRestorableWidget extends StatefulWidget {
@override
_MyRestorableWidgetState createState() => _MyRestorableWidgetState();
}
class _MyRestorableWidgetState extends State<MyRestorableWidget> with RestorationMixin {
final _counter = RestorableInt(0);
final _textFieldValue = RestorableString('');
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_counter, 'counter');
registerForRestoration(_textFieldValue, 'text_field_value');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Restorable Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter: ${_counter.value}'),
ElevatedButton(
onPressed: () {
_counter.value++;
},
child: Text('Increment'),
),
TextField(
controller: TextEditingController(text: _textFieldValue.value),
onChanged: (value) {
_textFieldValue.value = value;
},
),
],
),
),
);
}
}
registerForRestoration in the restoreState method to ensure that your properties are properly restored.
5. Registering Properties
In Flutter, state restoration involves registering properties that should be persisted across app sessions. This is typically done using the RestorationMixin or by implementing custom restoration logic.
| Property | Type | Description | Default Value |
|---|---|---|---|
| restorationId | String? |
Unique identifier for the restoration state. If not provided, a default ID is generated. | null |
| restorableProperties | List<RestorableProperty> |
List of properties to be restored. These must implement RestorableProperty. |
[] |
Example: Registering a Property
class MyWidget extends StatefulWidget with RestorationMixin {
@override
void restoreState(RestorationBucket? bucket, bool initialRestore) {
// Restore state from the bucket
}
@override
Widget build(BuildContext context) {
return Container(
// Example usage of restoration ID
restorationId: 'my_widget',
);
}
}
6. Using RestorableRouteFuture
RestorableRouteFuture is a utility class in Flutter that helps manage the restoration of asynchronous operations when a route is restored from a saved state. It ensures that futures initiated before the app was suspended or navigated away are properly resumed when the route is reactivated.
| Property | Description | Usage |
|---|---|---|
| future | The asynchronous operation to be restored. | RestorableRouteFuture<String>(future: fetchData()) |
| onError | Callback to handle errors during the future’s execution. | onError: (error) => print('Error: $error') |
| onSuccess | Callback to handle successful completion of the future. | onSuccess: (result) => setState(() => data = result) |
Example: Using RestorableRouteFuture in a Route
class MyRoute extends StatefulWidget {
@override
State<MyRoute> createState() => _MyRouteState();
}
class _MyRouteState extends State<MyRoute> with RestorableRouteFuture<String> {
late RestorableRouteFuture<String> _fetchFuture;
@override
void initState() {
super.initState();
_fetchFuture = RestorableRouteFuture<String>(future: fetchData());
}
Future<String> fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'Fetched Data';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Restorable Route')),
body: Center(
child: FutureBuilder<String>(
future: _fetchFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data!);
}
return CircularProgressIndicator();
},
),
),
);
}
}
RestorableRouteFuture is particularly useful in apps that rely heavily on asynchronous data fetching and need to maintain state across app suspensions or route navigations.
7. Implementing Form State Restoration
| Property | Description | Usage |
|---|---|---|
| restorationId | Unique identifier for restoring state | Assign to form widgets to enable restoration |
| autovalidateMode | Controls when validation occurs | Set to AutovalidateMode.onUserInteraction for real-time feedback |
Example: Form with Restoration
class MyForm extends StatefulWidget {
@override
_MyFormState createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> with RestorationMixin {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
@override
String get restorationId => 'my_form';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
_nameController.text = restorationBucket?.readString('name') ?? '';
_emailController.text = restorationBucket?.readString('email') ?? '';
}
@override
void saveState(RestorationBucket bucket) {
bucket.writeString('name', _nameController.text);
bucket.writeString('email', _emailController.text);
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
restorationId: 'my_form',
child: Column(
children: [
TextFormField(
controller: _nameController,
restorationId: 'name',
decoration: InputDecoration(labelText: 'Name'),
),
TextFormField(
controller: _emailController,
restorationId: 'email',
decoration: InputDecoration(labelText: 'Email'),
),
],
),
);
}
}
restorationId to capture their values.
RestorationMixin must be applied to the state class, not the widget.
8. Restoring Scroll Position
| Property | Description | Usage |
|---|---|---|
| primaryScrollController | Controls the primary scroll position for a scrollable widget. | primaryScrollController: ScrollController() |
| scrollDirection | Defines the direction of scrolling (vertical or horizontal). | scrollDirection: Axis.vertical |
Example: Restoring Scroll Position
class ScrollPositionRestorationExample extends StatefulWidget {
@override
_ScrollPositionRestorationExampleState createState() => _ScrollPositionRestorationExampleState();
}
class _ScrollPositionRestorationExampleState extends State<ScrollPositionRestorationExample> with RestorationMixin {
late RestorableInt _selectedIndex = RestorableInt(0);
late RestorableScrollController _scrollController = RestorableScrollController();
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_scrollController, 'scroll_controller');
registerForRestoration(_selectedIndex, 'selected_index');
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
controller: _scrollController,
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
}
RestorationMixin is required to restore scroll position across app sessions.
9. Testing State Restoration
Example: Testing with WidgetTester
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('State restoration test', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'my_scope',
home: Scaffold(
body: CounterWidget(),
),
),
);
// Simulate state change
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// Verify state is preserved
expect(find.text('1'), findsOneWidget);
});
}
| Test Method | Description | Notes |
|---|---|---|
| testWidgets | Flutter's testing utility for widget tests | Used to test UI components and their state behavior |
| pumpWidget | Builds and mounts a widget tree | Essential for initializing the test environment |
| tap | Simulates a tap gesture on a widget | Used to simulate user interaction that changes state |
| pumpAndSettle | Waits for all animations and microtasks to complete | Ensures state changes are fully processed before assertions |
restorationScopeId is consistent across test runs to properly simulate state restoration.
Testing Best Practices
- Use
testWidgetsfor comprehensive widget testing - Simulate user interactions with
tapandenterText - Use
pumpAndSettleto ensure all state changes are processed - Verify that state is correctly restored after app termination
10. Handling Restoration on Different Platforms
| Platform | Restoration Support | Notes |
|---|---|---|
| Android | NEW Full | Supports state restoration through AutomaticKeepAliveClientMixin and PageStorage. |
| iOS | NEW Full | Similar to Android, with AutomaticKeepAliveClientMixin and PageStorage. |
| Web | Partial | State restoration is limited due to browser behavior and session management. |
| Desktop (macOS, Windows, Linux) | Experimental | State restoration is not fully supported in desktop Flutter apps. |
| Web (PWA) | Partial | State can be restored using SharedPreferences or IndexedDB. |
Example: Platform-specific State Restoration
import 'package:flutter/material.dart';
class PlatformStateRestoration extends StatefulWidget {
@override
_PlatformStateRestorationState createState() => _PlatformStateRestorationState();
}
class _PlatformStateRestorationState extends State<PlatformStateRestoration> with AutomaticKeepAliveClientMixin {
int counter = 0;
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // Required for AutomaticKeepAliveClientMixin
return Scaffold(
appBar: AppBar(title: Text("State Restoration")),
body: Center(
child: Text("Counter: $counter"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
counter++;
});
},
child: Icon(Icons.add),
),
);
}
}
SharedPreferences or IndexedDB for persistent state storage.
PageStorage cautiously.