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 2 of 3 of this article. Click here to view the first page.

Enabling State Restoration on iOS

You need an extra iOS-specific step to enable state restoration. Open ios/Runner.xcodeproj in Xcode. Then, right-click the ios folder in Android Studio and select Flutter ▸ Open iOS module in Xcode. In Xcode, assign the Restoration ID like in the screenshot below:

Main storyboard restoration ID

The changes in the ios/Runner/Base.lproj/Main.storyboard XML file may include more than the restoration ID. It’s normal that saving the file in a different Xcode version introduces changes in the various lines.

Adding RestorationMixin

Open home_page.dart, and find // TODO: add the RestorationMixin. Extend a class with RestorationMixin:

class _HomePageState extends State<HomePage> with RestorationMixin {

Next, find // TODO: implement the RestorationMixin methods, and replace it with:

@override
String? get restorationId => 'home_page'; // 1

@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) { // 2
// TODO: implement the RestorationMixin methods
// TODO: register the list for restoration
// TODO: registering the scroll offset for restoration
// TODO: register the route for restoration 
}

In the code above, you have:

  1. The restorationId getter. The value should be unique across your app. Returning null disables the state restoration.
  2. The registerForRestoration method. You register your restorable properties here.

Repeat the same steps in add_item_page.dart. You can use add_item_page as the restoration ID there. Run the app by pressing Control-R to check if anything has broken.

Before you register the restorable properties, you have to create them. In the simplest cases, just change the field types to their restorable equivalents. For example, int to RestorableInt, TextEditingController to RestorableTextEditingController and so on. If there’s no appropriate class in the framework, you have to implement it yourself.

Implementing the Restorable ToDo Item List

You’ll start by creating the restorable ToDo items list. The restoration process starts with serializing. Serialization means converting to primitives, like int, double or String. Read more about primitives in the StandardMessageCodec documentation. The underlying native mechanisms can only handle the data in a serialized form. In the end, you need a reverse process: deserialization.

Replace // TODO: create the RestorableToDoItemList class in restorable_todo_item_list.dart with the following code snippet:

class RestorableToDoItemList extends RestorableValue<List<ToDoItem>> {
  @override
  List<ToDoItem> createDefaultValue() => []; // 1

  @override
  void didUpdateValue(List<ToDoItem>? oldValue) { // 2
    notifyListeners();
  }

  @override
  List<ToDoItem> fromPrimitives(Object? data) => data is! List // 3
      ? []
      : data
          .whereType<String>()
          .map((e) => ToDoItem.fromJson(jsonDecode(e)))
          .toList(growable: false);

  @override
  Object? toPrimitives() => // 4
      value.map((e) => jsonEncode(e)).toList(growable: false);
}

Several methods are used here:

  1. createDefaultValue, which returns a value to use when there’s no restoration data. In this case, it’s an empty list.
  2. From didUpdateValue, you notify the listeners. Usually, you can invoke notifyListeners() without any condition. But, if a primitive representation of the new and old values is the same, you can skip the notifications. This can happen, for example, if some fields of the class are excluded from serialization.
  3. fromPrimitives builds the instance of your class out of the raw data.
  4. toPrimitives does the opposite operation. Its implementation must be symmetrical to a previous one.

Restoring Main Page

It’s time to use the restorable list. Open main_page.dart, find // TODO: change the type to RestorableToDoItemList, and change the ToDo list field definition to the following:

class _HomePageState extends State<HomePage> with RestorationMixin {
  final _toDos = RestorableToDoItemList();

The list type is now a subtype of the RestorableProperty instead of the plain List. Next, change the direct access to the list to a value getter. Find // TODO: use value field of the list — note that there are two such instances. Replace the first with:

children: _toDos.value.isEmpty

And the second with:

List<Widget> _buildToDoList() => _toDos.value

Next, find // TODO: create a new instance of a list, and replace the list mutation with a new instance containing an appended item:

setState(() => _toDos.value = [..._toDos.value, item]);

Then, change // TODO: dispose the restorable list to a dispose method invocation:

_toDos.dispose();

Finally, register the list for restoration by replacing // TODO: register the list for restoration with:

registerForRestoration(_toDos, 'home_todos');

Run the app by pressing Control-R, and add some ToDos to the list. Now, perform the testing steps from the Getting Started section to check if the restoration works. You’ll see a result like in the screenshot below:

ToDo List restoration in action

Restore the Scroll Position

The framework has no class like RestorableScrollController. So, you have to also implement its restoration yourself. Flutter uses a declarative UI. You can’t query the SingleChildScrollView widget for its current scroll position, so you have to add ScrollController to access or set the scroll offset.

Open main_page.dart. Add a ScrollController along with its restorable offset in place of // TODO: add scroll offset and controller:

final _scrollOffset = RestorableDouble(0); // 1
final _scrollController = ScrollController(); // 2

In the code above, you have:

  1. The RestorableDouble for the scroll offset (position).
  2. The not restorable scroll controller.

Time to use them! In initState, find // TODO: listen to the scroll position changes, and replace it with:

_scrollController
      .addListener(() => _scrollOffset.value = _scrollController.offset); // 1
  WidgetsBinding.instance?.addPostFrameCallback(
      (_) => _scrollController.jumpTo(_scrollOffset.value)); // 2

The code may look complicated, but it’s actually very simple. Here’s what it contains:

  1. Scroll listener updating the restorable offset.
  2. Setting the restored scroll position on first initialization.

You have to bind a controller with a scrollable widget. Find // TODO: assign scroll controller, and insert the following code there:

controller: _scrollController,

Don’t forget to dispose the controller and offset. Replace // TODO: dispose the scroll controller and offset with disposal method calls:

_scrollController.dispose();
_scrollOffset.dispose();

To make it work, you need to register the scroll offset field for restoration. Change // TODO: registering the scroll offset for restoration to:

registerForRestoration(_scrollOffset, 'scroll_offset');

Note that the above function should be inside a restoreState method. Now, you can run the app and add some ToDos to the list to make it scrollable.

Note: You can enable multiwindow mode and/or enlarge the font scale to reduce the number of needed items.

Scroll through the list and perform the testing steps from the Getting Started section. It should look like this:

Restorable scroll position