DEV Community

loading...
Cover image for Introduction to State Restoration in Flutter

Introduction to State Restoration in Flutter

pedromassango profile image Pedro Massango Updated on ・4 min read

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?

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. Wrap our MaterialApp with a RootRestorationScope and provide its restorationId;
  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 car 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(
      RootRestorationScope( // Register a restoration scope for the entire app!
        restorationId: 'root',
        child: MyApp(),
      ),
    );

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      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 adn 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!

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.

Discussion

pic
Editor guide