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

Android and iOS can interrupt app processes to optimize resource usage. The system can kill apps in the background to gain more memory and CPU for the foreground. A killed app will start from scratch when a user brings it back. However, the user expects to see the same app state as when they left it instead of starting over again.

In this tutorial, you’ll see how to preserve the state of Flutter apps when the system decides to kill them. In the process, you’ll learn how to:

  • Set up the environment.
  • Discover the restorable states.
  • Implement apps with state restoration.
  • Test state restoration.
Note: You need some basic Flutter knowledge to follow this tutorial. If you’re an absolute beginner, check out the tutorial Getting Started With Flutter first.
Note: This tutorial assumes you’re working on macOS and building apps for both Android and iOS. However, you can also work on Linux or Windows and build for Android only. If so, ignore iOS-specific parts, and use the Shift-F10 key shortcut instead of Control-R. You can also build the same app for the web or desktop, but state restoration has no meaning on those platforms.

Getting Started

Download the starter project by clicking Download materials at the top or bottom of the tutorial.

In this tutorial, you’ll build a ToDo list app that lets you add a title and date for each item. Then, you’ll add the state restoration functionality, so you don’t lose any important data if the app closes unexpectedly.

First, you’ll explore the simple app. Open and run the starter project. You can use Control-R in Android Studio. Press the plus button and type a title in the Item details dialog.

A dialog on the top of the navigation stack and the text of the item title make up the in-memory state. So, now you’ll test the (lack of) restoration! You have to do it separately for both platforms.

Testing on Android

Go to Developer settings and turn on the option Don’t keep activities. Then, bring your app to the front. You’ll see the state loss — the app starts from scratch, and the dialog isn’t restored:

Don't keep activities option results

To simulate the restoration of a process, you have to send your app to the background. Then, bring another app to the foreground. Finally, return to your app. Returning from the recent app switcher without touching any other app isn’t enough. Also, don’t swipe out your app from the recents. The system won’t restore the state after that.

Note: Disable the Don’t keep activities option after the state restoration testing! Leaving it enabled may cause battery drain and data loss in other apps. A lot of apps don’t handle state restoration properly.

Testing on iOS

iOS doesn’t have the option to enforce process killing like Android. You have to perform some manual work. Start by opening ios/Runner.xcworkspace in Xcode. Set the Build Configuration to Profile, as on the screenshot below:

iOS app build configuration switching

Note that building the app in Profile mode takes more time than in Debug. In the case of the simple app from this tutorial, it should have no measurable impact. But, when you’re working on larger projects, you may want to use Debug mode by default. In such cases, you can switch to Profile mode only when needed.

Next, press the play button (or Command-R, not Control like in Android Studio!) to run the app. Press the plus button and type a title in the Item details modal. Send the app to the background by pressing the Home button or performing a gesture. Press the stop button (or Command-.) in Xcode. And finally, reopen the app by tapping its icon. Don’t use Xcode to launch the app at this stage!

Discovering the Restorable States

Looking for a state

Before you begin coding, think of what exactly the restorable parts of your app are. A good starting point is to look for StatefulWidgets. As the name suggests, they should contain mutable states. Note that only the in-memory state matters for restoration purposes. Look at the simple example with the checkbox:

Checkbox value as a persistent state

Here, you save the checked state instantly in a persistent way, somewhere like the local database, file or backend. So, it makes no sense to include it in the restoration logic, even if a checkbox is inside StatefulWidget. Now, look at the second example with a checkbox and a button to commit its state:

Checkbox value as an in-memory state

In this case, the state between tapping a checkbox and a button exists only in memory. So, it should be the subject of restoration. Other common sources of the restorable state include:

  • TextField (along with text obscuring states)
  • Radio buttons
  • Expandable and collapsible widgets (e.g., Drawer)
  • Scrollable containers (e.g., ListViews)

Note the last bullet. The scrollable container may be inside the StatelessWidget. Yet, its scroll position is an in-memory state. In such a case, you may want to convert your widget to a StatefulWidget and add a field for the ScrollController into its State.

The restorable state covers more than just the widget’s content. The navigation stack is also an in-memory state. Users expect to return to the same place they were before leaving the app. Note that dialogs — like pop-ups and modals — are on the stack too.

Implementing State Restoration

Note: Changes you applied by hot restart and hot reload features are lost when the app process is killed, just like an unpreserved in-memory state. Always cold start your app using Control-R or the play button before testing the state restoration.

Finally, you can get your hands dirty by locating MaterialApp in main.dart. Replace // TODO: replace with restorableScopeId with the following line of code:

restorationScopeId: 'app',

This can be any non-nullable string. If you want to test the changes done to the app, stop the app and rerun it with the help of Control-R or Command-R on macOS. Take a closer look, and you’ll see that there’s no visible effect yet. The restorationScopeId enables the state restoration ability for descendant widgets. It also turns on the basic navigation stack history restoration.