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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

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:

  1. The restorable route declaration.
  2. The onPresent callback for the navigation start.
  3. The onComplete callback for the navigation finish, which is called when you have a result.
  4. 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:

Restorable navigation route

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:

Restorable item details

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:

  1. The navigation argument serialization.
  2. 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!