DEV Community

Cover image for Introduction to State Restoration in Flutter
Pedro Massango
Pedro Massango

Posted on • Updated on

Introduction to State Restoration in Flutter

This is what we have from Apple's documentation:

Preserving your app’s user interface helps maintain the illusion that your app is always running. Interruptions can occur frequently on iOS devices, and a prolonged interruption might cause the system to terminate your app to free up resources. However, users do not know that your app has been terminated and will not expect the state of your app to change. Instead, they expect your app to be in the same state as when they left it. State preservation and restoration ensures that your app returns to its previous state when it launches again.

Even though this comes from Apple, the same applies for Android apps as well. You can read the related Android documentation here. Before we procceed, note that this is all about the user experience and as developers, it is our responsability to implement and provide a good UX to our users.

Ok, now that we have some knowleadge about what is SR and why it is important, let's se how it works in Flutter.

What about Flutter?

Back in the days, this was a difficult task because we didn't have official support from the framework, basically we had to implement a cache strategy ourself, and this was not an easy task. See issue #6827 on GitHub.

In Flutter 1.22, we now have Framework support for state restoration out of the box and this comes to save us a lot of time.

Flutter give us an synchronous API for us to provide the data that represents the state of our UI to be saved every time it changes so that any time our app is destroyed, the engine already has the latest information of or UI to be saved into the OS and restored if the user comes back to our app.

New concepts, same finality

If you come from Android (like me) or iOS, you will notice that the concepts are the same, we just have different ways of doing it.

Flutter hava the RestorationManager, which is the class that is responsible of handling all the state restoration work, usually we don't need to use it directly. RestorationBucket which is used to hold the piece of the restoration data that our app need to restore its state later. RestorationScope, which is used to provide a scoped RestorationBucket to its descendants, if the restorationId parameter is null then, the restoration capability is disabled for its descedants. RestorationMixin this is used by our widget's state, he is the one we will usually use, it provides a straightfoward API to save and restore our state. And finally we have restorable properties (one for each Dart's data type), which are used to represent the data to be stored in the buckets.

How to use it?

Flutter app widgets (MaterialApp, CupertinoApp and WidgetsApp) has a RootRestorationScope built-in by default so we don't need to provide one, instead we just need to provide an id that will be used by the inner root scope. Notice that without providing the restoration scope id, the state restoration feature will be turned off so make sure you don't miss this step.

One of the advantages of providing the restoration scope id into our app widget is that it also enables the Navigator built by the WidgetsApp to restore its state (i.e. to restore the history stack of active Routes). See the documentation on Navigator for more details around state restoration of Routes.

Let's see how we can use state restoration to save (and restore) the index of the BottomNavigationBar. Here is what we need to do:

  1. Provide a restorationScopeId to our MaterialApp (CupertinoApp and WidgetsApp also have this parameter);
  2. Our home page state should e mixed-in with RestorationMixin (and implement its members);
  3. Create our restorable properties and use it instead of the common Dart's data type.
  4. Resgister our restorable properties for restoration;

And that is it, Flutter will take care of the rest.

Example

The code bellow will save the index of the bottom navigation bar if the app is killed.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Give your RootRestorationScope an id, defaults to null.
      restorationScopeId: 'root', 
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

// Our state should be mixed-in with RestorationMixin
class _HomePageState extends State<HomePage> with RestorationMixin {

  // For each state, we need to use a restorable property
  final RestorableInt _index = RestorableInt(0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('Index is ${_index.value}')),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _index.value,
        onTap: (i) => setState(() => _index.value = i),
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home'
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.notifications),
              label: 'Notifications'
          ),
          BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Settings'
          ),
        ],
      ),
    );
  }

  @override
  // The restoration bucket id for this page,
  // let's give it the name of our page!
  String get restorationId => 'home_page';

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    // Register our property to be saved every time it changes,
    // and to be restored every time our app is killed by the OS!
    registerForRestoration(_index, 'nav_bar_index');
  }
}
Enter fullscreen mode Exit fullscreen mode

How to test?

If you want to test this code (or your code during development), you will need to enable the "Don't keep activities" options in your device's Developer Options.

  1. Open the Developer options settings, and enable the option Don't keep activities.

Alt Text

This will simulate the lack of memory and the device will kill all activities as soon as you leave it.

  1. Run the code above app and tap the Settings item;
  2. Go to the launcher by pressing the device's home button. Press the overview button and return to the app.
  3. notice that the Settings item is still selected.

With State Restoration

Note: You can try the same app without state restoration (find the code here), where if you run it and repeat the same steps you will notice that the index is always back to Homeonce you launch the app again. Make sure you stop and run the code code again because state restoration code is not removed with hot-reload/restart as it is tied to the host OS!

Without State Restoration

That is it for today. There is still a lot of things to talk about state restoration and I will split into more than one article so make sure you follow me (here or on Twitter) to not miss the next one.

Top comments (3)

Collapse
 
azurabennett profile image
Azura Bennett

State restoration in Flutter, akin to iOS and Android, preserves UI states seamlessly. With built-in support, Flutter alleviates manual cache implementation, enhancing user experience sewage effectively.

Collapse
 
joaoabrantis profile image
João Abrantes

Very useful and well written! When are the next articles coming out? Would love to see some examples on how to restore Routes and BLoCs :)

Collapse
 
themonkslab profile image
themonkslab

Super useful and well written article!