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:
- Provide a
restorationScopeId
to ourMaterialApp
(CupertinoApp
andWidgetsApp
also have this parameter); - Our home page state should e mixed-in with
RestorationMixin
(and implement its members); - Create our restorable properties and use it instead of the common Dart's data type.
- 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');
}
}
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.
- Open the Developer options settings, and enable the option
Don't keep activities
.
This will simulate the lack of memory and the device will kill all activities as soon as you leave it.
- Run the code above app and tap the
Settings
item; - Go to the launcher by pressing the device's home button. Press the overview button and return to the app.
- notice that the
Settings
item is still selected.
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 Home
once 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!
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)
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.
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 :)
Super useful and well written article!