State Restoration of Flutter App
Android and iOS interrupt application processes to optimize resource usage by killing the app, losing the app’s state. Here, you’ll explore clever state restoration techniques in Flutter. By Karol Wrótniak.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
State Restoration of Flutter App
20 mins
- Getting Started
- Testing on Android
- Testing on iOS
- Discovering the Restorable States
- Implementing State Restoration
- Enabling State Restoration on iOS
- Adding RestorationMixin
- Implementing the Restorable ToDo Item List
- Restoring Main Page
- Restore the Scroll Position
- Implementing the Restorable Route Returning Result
- Implementing Simple Restorable Properties
- Adding State to Date Picker Restoration
- Where to Go From Here?
Implementing the Restorable Route Returning Result
The last modification on the main page refers to the navigation to the add item page. Adding restorationScopeId
to the app enables navigation route restoration. But it doesn’t cover returning the results from other pages. To fill that gap, find // TODO: replace with restorable route
in a main_page.dart file, and add the following fields:
late final _addItemRoute = RestorableRouteFuture<ToDoItem?>( // 1
onPresent: (navigator, arguments) => navigator.restorablePush( // 2
_addItemDialogRouteBuilder,
arguments: arguments,
),
onComplete: (ToDoItem? item) { // 3
if (item != null) {
setState(() => _toDos.value = [..._toDos.value, item]);
}
});
static DialogRoute<ToDoItem?> _addItemDialogRouteBuilder( // 4
BuildContext context,
Object? arguments,
) => DialogRoute(
context: context,
builder: (_) => const AddItemPage(),
);
In the code above, you have:
- The restorable route declaration.
- The
onPresent
callback for the navigation start. - The
onComplete
callback for the navigation finish, which is called when you have a result. - The
static
route builder. If you pass a non-static function here, code will compile, but you’ll get a runtime error.
Use that route in place of // TODO: present restorable route
:
onPressed: _addItemRoute.present,
tooltip: 'Add item',
You have to dispose the route like any other restorable property. Replace // TODO: dispose the route
with:
_addItemRoute.dispose();
And finally, register the route for restoration by replacing // TODO: register the route for restoration
with:
registerForRestoration(_addItemRoute, 'add_item_route');
Run the app, and tap the floating action button. Perform the testing steps from the Getting Started section. You’ll see a result like this:
Implementing Simple Restorable Properties
Open add_item_page.dart. It has two properties: a text editing controller holding the title and a date that came from the picker. Both properties have restorable versions in the framework. In the case of a text editing controller, the code changes are straightforward. First, replace TODO: add the RestorationMixin
with:
class _AddItemPageState extends State<AddItemPage> with RestorationMixin {
Next, change TextEditingController
to its restorable version, RestorableTextEditingController
. Find // TODO: replace with restorable controller
, and change the line to:
final _controller = RestorableTextEditingController();
Analogously, use RestorableDateTime
in place of // TODO: replace with restorable date
:
final _dueDate = RestorableDateTime(DateTime.now());
You can’t use the new fields directly. Find the lines with // TODO: replace with value property
, and change them accordingly:
controller: _controller.value,
//...
child: Text(DateFormat.yMd().format(_dueDate.value)),
//...
_controller.value.text,
//...
_dueDate.value,
Don’t forget to dispose
a restorable date. Change // TODO: dispose the date
to:
_dueDate.dispose();
Finally, set the restoration ID and register the properties for restoration. Find // TODO: implement the RestorationMixin members
, and replace it with:
@override
String? get restorationId => 'add_item_page';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_controller, 'title');
registerForRestoration(_dueDate, 'due_date');
// TODO: register route for restoration
}
Run the app, and tap the floating action button. Then, type a title and choose a date. Finally, perform the testing steps from the Getting Started section. The result should look like this:
The field for a date is final. You don’t modify the restorable date itself, but its underlying value. Note the default value of the date. There’s no distinction between the value you pick and that default.
Consider a case where you open an item, add dialog and send the app to the background immediately. Then, you return two days later, and the app process was killed in the meantime. Finally, after restoration, you’ll see the date two days in the past. In some cases, you may want to not save and restore the value when a user hasn’t selected anything yet.
Adding State to Date Picker Restoration
The last — but not the least — part of this tutorial is about the restorable route to the DatePicker
. Like on the previous page, find // TODO: replace with restorable route
, remove the callback, and add the fields:
late final RestorableRouteFuture<DateTime?> _restorableDatePickerRouteFuture =
RestorableRouteFuture<DateTime?>(
onComplete: (newDate) {
if (newDate != null) {
setState(() => _dueDate.value = newDate);
}
},
onPresent: (NavigatorState navigator, Object? arguments) =>
navigator.restorablePush(
_datePickerRoute,
arguments: _dueDate.value.millisecondsSinceEpoch, // 1
),
);
static Route<DateTime> _datePickerRoute(
BuildContext context,
Object? arguments,
) => DialogRoute<DateTime>(
context: context,
builder: (context) => DatePickerDialog(
restorationId: 'date_picker_dialog',
initialEntryMode: DatePickerEntryMode.calendarOnly,
initialDate: DateTime.fromMillisecondsSinceEpoch(arguments! as int), // 2
firstDate: DateTime.now(),
lastDate: DateTime(2243),
),
);
In the code above, you have:
- The navigation argument serialization.
- The navigation result deserialization.
You not only receive a result here but also pass an initial date as an argument. The DateTime
class isn’t primitive, so it’s not serializable using StandardMessageCodec
. That’s why you have to pass it as the number of seconds since the Unix epoch: January 1, 1970. The year of last date (2243) is just a maximum supported value.
Use the route
in place of // TODO: present restorable route
:
onTap: _restorableDatePickerRouteFuture.present,
Next, dispose the route. Replace // TODO: dispose the route
with:
_restorableDatePickerRouteFuture.dispose();
Finally, register the route
for restoration in place of // TODO: register route for restoration
in a restoreState
method:
registerForRestoration(_restorableDatePickerRouteFuture, 'date_picker_route_future');
Where to Go From Here?
You made it through the entire tutorial about state restoration in Flutter! Get the complete code for this tutorial by clicking Download materials at the top or bottom of the tutorial.
You’ve gotten a great start on state restoration, but this tutorial doesn’t cover all the capabilities of the state restoration API. There are more classes, like RestorationBucket
. Some classes have more methods, like RestorationMixin.didToggleBucket
. Some methods have more parameters, like oldBucket
and initialRestore
of RestorationMixin.restoreState
. You may find them useful in advanced use cases.
A good starting point in the official Flutter documentation is the RestorationManager
page. You can go forward from there by following the links to the next classes.
Want to learn more about state restoration in the native platforms? Check out our other tutorials: State Restoration in SwiftUI for iOS and Jetpack Saved State for ViewModel: Getting Started for Android.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!