From a7f6c5062b7c0ba9f22232389b0bb6125174cf78 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Thu, 9 Mar 2023 10:10:07 -0800 Subject: [PATCH 01/10] Stashing --- .../android/restore-state-android.md | 349 ++++++++++++++++++ .../ios/restore-state-ios.md | 133 +++++++ 2 files changed, 482 insertions(+) create mode 100644 src/development/platform-integration/android/restore-state-android.md create mode 100644 src/development/platform-integration/ios/restore-state-ios.md diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md new file mode 100644 index 00000000000..3f2e1cb907e --- /dev/null +++ b/src/development/platform-integration/android/restore-state-android.md @@ -0,0 +1,349 @@ +--- +title: "State restoration for Android apps" +description: "How to restore the state of your Android app after it's been killed by the OS." +--- + +When a user runs a mobile app and then selects another +app to run, the first app is moved to the background, +or _backgrounded_. The operating system (both iOS and Android) +often kill the backgrounded app to release memory or +improve performance for the app running in the foreground. + +When the user selects the app again, bringing it +back to the foreground, the OS relaunches it. +But, unless you've set up a way to save the +state of the app before it was sent to the background, +you've lost the state and the app starts from +scratch. The user has lost the continuity they expect +and are generally annoyed. (Imagine filling out a +lengthy form and being interrupted by a phone call +_before_ clicking **Submit**...) + +So, how can you restore the state of the app so that +it looks like it did before it was sent to the +background? + +{% comment %} +xxx: Instance state vs long-lived state (2:22:28) +xxx: Instance state: +xxx: UI state: active tab, current form field values +xxx: Limited to <1MB on Android; if you go over +xxx: your app will crash with TransactionToolLargeException +xxx: thrown in the native code. +xxx: Long-lived state: +xxx: User's login token +xxx: Apps local data +xxx: use state_persistence package +{% endcomment %} + +Flutter has a solution for that scenario with the +[`RestorationManager`][] (and related classes) +in the [services][] library. +With the `RestorationManager`, the Flutter Framework +provides the state data to the engine _as the state +changes_, so that the app is ready when the OS signals +that it's about to kill the app, giving the app only +moments to prepare. + +The `RestorationManager` manages the restoration +data, which is serialized to a tree of [`RestorationBucket`][], +that are synchronized with the Flutter engine. +A `RestorationBucket` contains a key/value pair +of state information that you want to save. The key is a +[`restorationId`][] that is unique to that `RestorationScope`. + +Flutter automatically creates a +[`RestorationScope`][] for each route in your +app to track the `RestorationBucket`s in that scope. +In general, you shouldn't need to worry about +this object _unless_ you have two (or more) buckets with +the same `restorationId`. If that occurs, you +can create a new `RestorationScope` to ensure +that each ID is unique to its scope. +Each scope is assigned a unique [`restorationScopeId`][]. + +You must decide what state you want to save and restore, +but you can only save specific types to a `RestorationBucket`, +namely: null, bool, int, double, String, Uint8List +(and other typed data), List, Map, and child `RestorationBucket`s. + +[`restorationId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationId.html +[`restorationScopeId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationScopeId.html +[`RestorationScope`]: {{site.api}}/flutter/widgets/RestorationScope-class.html + +## Step 1: Enabling state restoration + +Most every Flutter app contains a `main` method that calls the +[`runApp`][] function. You can use `runApp` +to set up the framework and the bindings that bind +the Dart code to the native Flutter engine. +In this case, set up the `RestorationManager` in the +`runApp` function. For example: + +```dart +void main() { + /... + + runApp( + const RootRestorationScope( + restorationId: 'root', + child: VeggieApp(), + ), + ); +} +``` + +QUESTION: I thought that Flutter automatically created + the Scope for every route. But this app manually + creats a Scope for the page... (xxx) + +[`runApp`]: {{site.api}}/flutter/widgets/runApp.html + +## Step 2: Add RestorableMixin to the State class + +The easiest way to implement restorable state is to +add the [`RestorableMixin`][] to the app's `State` class. + +```dart +class _VeggieAppState extends State with RestorationMixin +``` + +Once you've added the restore state mixin, +your IDE asks you to implement an abstract method +called [`restoreState`][]. The `restoreState` method calls +[`registerForRestoration`][] to register that you +intend to save state. For example: + +```dart +@override +void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_appState, 'state'); +} +``` + +[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html +[`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html + +## Step 3: Define a restorationScopeId for each scope + +Each Scope in the app should have a `restorationScopeId`, +meaning that, at a minimum, you will have one scope per +route. If you create additional scopes +(to avoid `restorationId` duplication, for example), +then those are also assigned a unique ID. + +```dart +restorationScopeId: 'app', +``` + +Also, if you want your app to return to the page that the +user was most recently viewing, then you need to implement +restoration state for the navigation, as well. +This page won't cover navigation, but the +accompanying example, VeggieSeasons, implements this +feature using the [go_router][] package. + +[go_router]: {{site.pub}}/packages/go_router + +## Step 4: + +Most of the state restoration work occurs in the +`build` method on the `State` class that implements +`RestorationMixin`. Your primary task here is to +define a `restorationId` for each piece of state +you want to save. + + +```dart +class _VeggieAppState extends State with RestorationMixin { + final _RestorableAppState _appState = _RestorableAppState(); + + @override + String get restorationId => 'wrapper'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_appState, 'state'); + } + + @override + void dispose() { + _appState.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: _appState.value, + ), + ChangeNotifierProvider( + create: (_) => Preferences()..load(), + ), + ], + child: CupertinoApp.router( + theme: Styles.veggieThemeData, + debugShowCheckedModeBanner: false, + restorationScopeId: 'app', + routerConfig: GoRouter( + navigatorKey: _rootNavigatorKey, + restorationScopeId: 'router', + initialLocation: '/list', + redirect: (context, state) { + if (state.path == '/') { + return '/list'; + } + return null; + }, + debugLogDiagnostics: true, + routes: [ + ShellRoute( + navigatorKey: _shellNavigatorKey, + pageBuilder: (context, state, child) { + return CupertinoPage( + restorationId: 'router.shell', + child: HomeScreen( + restorationId: 'home', + child: child, + onTap: (index) { + if (index == 0) { + context.go('/list'); + } else if (index == 1) { + context.go('/favorites'); + } else if (index == 2) { + context.go('/search'); + } else { + context.go('/settings'); + } + }, + ), + ); + }, + routes: [ + GoRoute( + path: '/list', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + restorationId: 'route.list', + child: const ListScreen(restorationId: 'list'), + ); + }, + routes: [ + _buildDetailsRoute(), + ], + ), + GoRoute( + path: '/favorites', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + restorationId: 'route.favorites', + child: const FavoritesScreen(restorationId: 'favorites'), + ); + }, + routes: [ + _buildDetailsRoute(), + ], + ), + GoRoute( + path: '/search', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + restorationId: 'route.search', + child: const SearchScreen(restorationId: 'search'), + ); + }, + routes: [ + _buildDetailsRoute(), + ], + ), + GoRoute( + path: '/settings', + pageBuilder: (context, state) { + return FadeTransitionPage( + key: state.pageKey, + restorationId: 'route.settings', + child: const SettingsScreen(restorationId: 'settings'), + ); + }, + routes: [ + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'categories', + pageBuilder: (context, state) { + return VeggieCategorySettingsScreen.pageBuilder( + context); + }, + ), + GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'calories', + pageBuilder: (context, state) { + return CalorieSettingsScreen.pageBuilder(context); + }, + ), + ], + ), + ], + ), + ], + ), + ), + ); + } + + // GoRouter does not support relative routes, + // see https://github.com/flutter/flutter/issues/108177 + GoRoute _buildDetailsRoute() { + return GoRoute( + parentNavigatorKey: _rootNavigatorKey, + path: 'details/:id', + pageBuilder: (context, state) { + final veggieId = int.parse(state.params['id']!); + return CupertinoPage( + restorationId: 'route.details', + fullscreenDialog: true, + child: DetailsScreen( + id: veggieId, + restorationId: 'details', + ), + ); + }, + ); + } +} + +``` +## Testing state restoration + +To test state restoration, set up your mobile device so that +it doesn't save state once an app is backgrounded. +To learn how to do this for iOS and Android, check out +[Testing state restoration][] on the +[`RestorationManager`][] page. Don't forget to reenable +storing state once you are finished with testing! + +[Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration + +[`FlutterViewController`]: {{site.api}}/objcdoc/Classes/FlutterViewController.html +[`RestorationBucket`]: {{site.api}}/flutter/services/RestorationBucket-class.html +[`RestorationManager`]: {{site.api}}/flutter/services/RestorationManager-class.html +[services]: {{site.api}}/flutter/services/services-library.html + +## Other resources + +The code in this page was taken from the [VeggieSeasons][] app. +VeggieSeasons is a sample app written for iOS that uses Cupertino +widgets. An iOS app requires [a bit of extra setup][] in Xcode, but +the restoration classes otherwise work the same on both iOS and Android. + +You might also want to check out packages on pub.dev that +perform state restoration, such as [`statePersistence`][]. + +[`statePersistence`]: {{site.pub-pkg}}/state_persistence +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons + diff --git a/src/development/platform-integration/ios/restore-state-ios.md b/src/development/platform-integration/ios/restore-state-ios.md new file mode 100644 index 00000000000..ae4e9d689de --- /dev/null +++ b/src/development/platform-integration/ios/restore-state-ios.md @@ -0,0 +1,133 @@ +--- +title: "State restoration on iOS" +description: "How to restore the state of your iOS app after it's been killed by the OS." +--- + +When a user runs a mobile app and then selects another +app to run, the first app is moved to the background, +or _backgrounded_. The operating system (both iOS and Android) +often kill the backgrounded app to release memory or +improve performance for the app running in the foreground. + +When the user selects the app again, bringing it +back to the foreground, the OS relaunches it. +But, unless you've set up a way to save the +state of the app before it was sent to the background, +you've lost the state and the app starts from +scratch. The user has lost the continuity they expect +and are generally annoyed. (Imagine filling out a +lengthy form and being interrupted by a phone call +before hitting **Submit**...) + +So, how can you restore the state of the app so that +it looks like it did before it was sent to the +background? + +Flutter has a solution for that scenario with the +[`RestorationManager`][] (and related) classes +in the [services][] library. +With the `RestorationManager`, the Flutter Framework +provides the state data to the engine _as the state +changes_, so that the app is ready when the OS signals +that it's about to kill the app. + +The `RestorationManager` manages the restoration +data, which is serialized to a tree of [`RestorationBuckets`[], +that are synchronized with the Flutter engine. + +You must decide what state you want to save and restore, +but you can only save specific types to a `RestorationBucket`, +namely: null, bool, int, double, String, Uint8List +(and other typed data), List, Map, and child `RestorationBuckets`. + +## Enabling state restoration + +Most every Flutter app contains a `main` method that calls the +[`runApp`][] function. You can use `runApp` +to set up the framework and the bindings that bind +the Dart code to the native Flutter engine. +In this case, set up the `RestorationManager` in the +`runApp` function. For example: + +```dart +void main() { + /... + + runApp( + const RootRestorationScope( + restorationId: 'root', + child: myApp(), + ), + ); +} +``` + +[`runApp`]: {{site.api}}/flutter/widgets/runApp.html + +## Add RestorableMixin to the State class + +The easiest way to implement restorable state is to +add the [`RestorableMixin`][] to the app's `State` class. + +```dart +class _MyAppState extends State with RestorationMixin +``` + +Once you've added the restore state mixin, +your IDE asks you to implement an abstract method +called [`restoreState`][]. The `restoreState` method calls +[`registerForRestoration`][] to register a property +that you want to save. For example: + +```dart +@override +void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_appState, 'state'); +} +``` + +Call `registerForRestoration` once for every +piece of state that you want to save and restore. + +--- +You might also want to check out packages on pub.dev that +help with state restoration, such as [`statePersistence`][]. + +[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html +[`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html +[`statePersistence`]: {{site.pub-pkg}}/state_persistence + +## Testing state restoration + +For advice on testing, see +[Testing state restoration][] on the +[`RestorationManager`][] page. + +[Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration + +--------------------- + +## Enabling state restoration + +To enable state restoration on iOS, +a restoration identifier has to be assigned to the +[`FlutterViewController`][]. +If you are using the standard embedding (produced by flutter create), +use with the following steps: + +1. In the app's directory, open `ios/Runner.xcodeproj` with Xcode. +1. Select `Main.storyboard` under **Runner/Runner** + in the Project Navigator on the left. +1. Select the Flutter View Controller under + **Flutter View Controller Scene** in the view hierarchy. +1. Navigate to the **Identity Inspector** in the panel on the right. +1. Enter a unique restoration ID in the provided field. +1. Save the project. + + +[`FlutterViewController`]: {{site.api}}/objcdoc/Classes/FlutterViewController.html +[`RestorationBuckets`]: {{site.api}}/flutter/services/RestorationBucket-class.html +[`RestorationManager`]: {{site.api}}/flutter/services/RestorationManager-class.html +[services]: {{site.api}}/flutter/services/services-library.html + + From 7e4d333feeca6f1a86f3719e641f2c48900ade76 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Wed, 22 Mar 2023 12:49:23 -0700 Subject: [PATCH 02/10] Adding state restoration pages --- src/_data/sidenav.yml | 6 +- .../android/restore-state-android.md | 279 +++++------------- .../ios/restore-state-ios.md | 128 +------- 3 files changed, 86 insertions(+), 327 deletions(-) diff --git a/src/_data/sidenav.yml b/src/_data/sidenav.yml index bc1719ea91a..22fe16a984f 100644 --- a/src/_data/sidenav.yml +++ b/src/_data/sidenav.yml @@ -170,6 +170,8 @@ permalink: /development/platform-integration/android/androidx-migration - title: Deprecated Splash Screen API Migration permalink: /development/platform-integration/android/splash-screen-migration + - title: Restore state on Android **NEW** + permalink: /development/platform-integration/ios/restore-state-android - title: Targeting ChromeOS with Android permalink: /development/platform-integration/android/chromeos - title: iOS @@ -189,6 +191,8 @@ permalink: /development/platform-integration/ios/platform-views - title: iOS debugging permalink: /development/platform-integration/ios/ios-debugging + - title: Restore state on iOS **NEW** + permalink: /development/platform-integration/ios/restore-state-ios - title: Linux permalink: /development/platform-integration/linux children: @@ -340,7 +344,7 @@ children: - title: Debugging tools permalink: /testing/debugging - - title: Testing plugins **NEW** + - title: Testing plugins permalink: /testing/testing-plugins - title: Debugging apps programmatically permalink: /testing/code-debugging diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 3f2e1cb907e..94c78983989 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -12,30 +12,17 @@ improve performance for the app running in the foreground. When the user selects the app again, bringing it back to the foreground, the OS relaunches it. But, unless you've set up a way to save the -state of the app before it was sent to the background, +state of the app before it was killed, you've lost the state and the app starts from -scratch. The user has lost the continuity they expect -and are generally annoyed. (Imagine filling out a -lengthy form and being interrupted by a phone call -_before_ clicking **Submit**...) +scratch. The user has lost the continuity they expect, +which is clearly not ideal. +(Imagine filling out a lengthy form and being interrupted +by a phone call _before_ clicking **Submit**.) So, how can you restore the state of the app so that it looks like it did before it was sent to the background? -{% comment %} -xxx: Instance state vs long-lived state (2:22:28) -xxx: Instance state: -xxx: UI state: active tab, current form field values -xxx: Limited to <1MB on Android; if you go over -xxx: your app will crash with TransactionToolLargeException -xxx: thrown in the native code. -xxx: Long-lived state: -xxx: User's login token -xxx: Apps local data -xxx: use state_persistence package -{% endcomment %} - Flutter has a solution for that scenario with the [`RestorationManager`][] (and related classes) in the [services][] library. @@ -45,6 +32,24 @@ changes_, so that the app is ready when the OS signals that it's about to kill the app, giving the app only moments to prepare. +{{site.alert.secondary}} + Instance state vs long-lived state + When should you use the `RestorationManager` and + when should you save state to long term storage? + _Instance state_ or + (also called _short-term_ or _ephemeral_ state), + includes unsubmitted form field values, the currently + selected tab, and so on. On Android, this is + limited to 1 MB and, if the app exceeds this, + it crashes with a `TransactionTooLargeException` + error in the native code. + To learn more about short term and long term state, + check out [Differentiate between ephemeral state + and app state][state]. +{{site.alert.end}} + +[state]: {{site.url}}/development/data-and-backend/state-mgmt/ephemeral-vs-app + The `RestorationManager` manages the restoration data, which is serialized to a tree of [`RestorationBucket`][], that are synchronized with the Flutter engine. @@ -56,20 +61,30 @@ Flutter automatically creates a [`RestorationScope`][] for each route in your app to track the `RestorationBucket`s in that scope. In general, you shouldn't need to worry about -this object _unless_ you have two (or more) buckets with -the same `restorationId`. If that occurs, you -can create a new `RestorationScope` to ensure +this object _unless_ the scope has two (or more) buckets with +the same `restorationId`. If that occurs, +create a new `RestorationScope` to ensure that each ID is unique to its scope. Each scope is assigned a unique [`restorationScopeId`][]. You must decide what state you want to save and restore, but you can only save specific types to a `RestorationBucket`, -namely: null, bool, int, double, String, Uint8List -(and other typed data), List, Map, and child `RestorationBucket`s. +namely: `null`, `bool`, `int`, `double`, `String`, `Uint8List` +(and other typed data), `List`, `Map`, and child `RestorationBucket`s. + +{{site.alert.note}} + The code in this page was taken from [VeggieSeasons][]. + VeggieSeasons is a sample app written for iOS that uses Cupertino + widgets. An iOS app requires [a bit of extra setup][] in Xcode, but + the restoration classes otherwise work the same on both iOS and Android. +{{site.alert.end}} + +[a bit of extra setup]: {{site.api}}/flutter/services/RestorationManager-class.html#state-restoration-on-ios [`restorationId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationId.html [`restorationScopeId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationScopeId.html [`RestorationScope`]: {{site.api}}/flutter/widgets/RestorationScope-class.html +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons ## Step 1: Enabling state restoration @@ -77,8 +92,8 @@ Most every Flutter app contains a `main` method that calls the [`runApp`][] function. You can use `runApp` to set up the framework and the bindings that bind the Dart code to the native Flutter engine. -In this case, set up the `RestorationManager` in the -`runApp` function. For example: +Set up the `RestorationManager` in the `runApp` function. +For example: ```dart void main() { @@ -93,16 +108,16 @@ void main() { } ``` -QUESTION: I thought that Flutter automatically created +QUESTION for Michael: I thought that Flutter automatically created the Scope for every route. But this app manually - creats a Scope for the page... (xxx) + creats a Scope for the page. ?? (xxx) [`runApp`]: {{site.api}}/flutter/widgets/runApp.html -## Step 2: Add RestorableMixin to the State class +## Step 2: Add RestorationMixin to the State class -The easiest way to implement restorable state is to -add the [`RestorableMixin`][] to the app's `State` class. +To implement restorable state, +add the [`RestorationMixin`][] to the app's `State` class. ```dart class _VeggieAppState extends State with RestorationMixin @@ -122,6 +137,7 @@ void restoreState(RestorationBucket? oldBucket, bool initialRestore) { ``` [`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html +[`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html [`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html ## Step 3: Define a restorationScopeId for each scope @@ -130,22 +146,29 @@ Each Scope in the app should have a `restorationScopeId`, meaning that, at a minimum, you will have one scope per route. If you create additional scopes (to avoid `restorationId` duplication, for example), -then those are also assigned a unique ID. +then also assign those with a unique ID. ```dart restorationScopeId: 'app', ``` -Also, if you want your app to return to the page that the -user was most recently viewing, then you need to implement +## Step 4: Restoring navigation state + +If you want your app to return to the route (such +as a specific tab) that the user was most recently viewing, +then you need to implement restoration state for the navigation, as well. This page won't cover navigation, but the accompanying example, VeggieSeasons, implements this feature using the [go_router][] package. +For more information on navigation and the +`go_router` package, check out [Navigation and routing][]. + [go_router]: {{site.pub}}/packages/go_router +[Navigation and routing]: {{site.url}}/development/ui/navigation -## Step 4: +## Step 4: Save the state you want to restore Most of the state restoration work occurs in the `build` method on the `State` class that implements @@ -153,179 +176,25 @@ Most of the state restoration work occurs in the define a `restorationId` for each piece of state you want to save. +In the VeggieSeasons example, most of this work +is implemented in the `lib/screens` classes. -```dart -class _VeggieAppState extends State with RestorationMixin { - final _RestorableAppState _appState = _RestorableAppState(); - - @override - String get restorationId => 'wrapper'; - - @override - void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(_appState, 'state'); - } - - @override - void dispose() { - _appState.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider.value( - value: _appState.value, - ), - ChangeNotifierProvider( - create: (_) => Preferences()..load(), - ), - ], - child: CupertinoApp.router( - theme: Styles.veggieThemeData, - debugShowCheckedModeBanner: false, - restorationScopeId: 'app', - routerConfig: GoRouter( - navigatorKey: _rootNavigatorKey, - restorationScopeId: 'router', - initialLocation: '/list', - redirect: (context, state) { - if (state.path == '/') { - return '/list'; - } - return null; - }, - debugLogDiagnostics: true, - routes: [ - ShellRoute( - navigatorKey: _shellNavigatorKey, - pageBuilder: (context, state, child) { - return CupertinoPage( - restorationId: 'router.shell', - child: HomeScreen( - restorationId: 'home', - child: child, - onTap: (index) { - if (index == 0) { - context.go('/list'); - } else if (index == 1) { - context.go('/favorites'); - } else if (index == 2) { - context.go('/search'); - } else { - context.go('/settings'); - } - }, - ), - ); - }, - routes: [ - GoRoute( - path: '/list', - pageBuilder: (context, state) { - return FadeTransitionPage( - key: state.pageKey, - restorationId: 'route.list', - child: const ListScreen(restorationId: 'list'), - ); - }, - routes: [ - _buildDetailsRoute(), - ], - ), - GoRoute( - path: '/favorites', - pageBuilder: (context, state) { - return FadeTransitionPage( - key: state.pageKey, - restorationId: 'route.favorites', - child: const FavoritesScreen(restorationId: 'favorites'), - ); - }, - routes: [ - _buildDetailsRoute(), - ], - ), - GoRoute( - path: '/search', - pageBuilder: (context, state) { - return FadeTransitionPage( - key: state.pageKey, - restorationId: 'route.search', - child: const SearchScreen(restorationId: 'search'), - ); - }, - routes: [ - _buildDetailsRoute(), - ], - ), - GoRoute( - path: '/settings', - pageBuilder: (context, state) { - return FadeTransitionPage( - key: state.pageKey, - restorationId: 'route.settings', - child: const SettingsScreen(restorationId: 'settings'), - ); - }, - routes: [ - GoRoute( - parentNavigatorKey: _rootNavigatorKey, - path: 'categories', - pageBuilder: (context, state) { - return VeggieCategorySettingsScreen.pageBuilder( - context); - }, - ), - GoRoute( - parentNavigatorKey: _rootNavigatorKey, - path: 'calories', - pageBuilder: (context, state) { - return CalorieSettingsScreen.pageBuilder(context); - }, - ), - ], - ), - ], - ), - ], - ), - ), - ); - } - - // GoRouter does not support relative routes, - // see https://github.com/flutter/flutter/issues/108177 - GoRoute _buildDetailsRoute() { - return GoRoute( - parentNavigatorKey: _rootNavigatorKey, - path: 'details/:id', - pageBuilder: (context, state) { - final veggieId = int.parse(state.params['id']!); - return CupertinoPage( - restorationId: 'route.details', - fullscreenDialog: true, - child: DetailsScreen( - id: veggieId, - restorationId: 'details', - ), - ); - }, - ); - } -} +QUESTION for Michael: This part of VeggieSeasons is fairly dense. +I'm not sure what is a good code excerpt to include here. (xxx) -``` ## Testing state restoration To test state restoration, set up your mobile device so that it doesn't save state once an app is backgrounded. -To learn how to do this for iOS and Android, check out -[Testing state restoration][] on the -[`RestorationManager`][] page. Don't forget to reenable -storing state once you are finished with testing! +To learn how to do this for both iOS and Android, +check out [Testing state restoration][] on the +[`RestorationManager`][] page. + +{{site.alert.note}} + Don't forget to reenable + storing state on your device once you are + finished with testing! +{{site.alert.end}} [Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration @@ -336,14 +205,8 @@ storing state once you are finished with testing! ## Other resources -The code in this page was taken from the [VeggieSeasons][] app. -VeggieSeasons is a sample app written for iOS that uses Cupertino -widgets. An iOS app requires [a bit of extra setup][] in Xcode, but -the restoration classes otherwise work the same on both iOS and Android. - -You might also want to check out packages on pub.dev that +You might want to check out packages on pub.dev that perform state restoration, such as [`statePersistence`][]. [`statePersistence`]: {{site.pub-pkg}}/state_persistence -[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons diff --git a/src/development/platform-integration/ios/restore-state-ios.md b/src/development/platform-integration/ios/restore-state-ios.md index ae4e9d689de..89b5f78f17a 100644 --- a/src/development/platform-integration/ios/restore-state-ios.md +++ b/src/development/platform-integration/ios/restore-state-ios.md @@ -9,125 +9,17 @@ or _backgrounded_. The operating system (both iOS and Android) often kill the backgrounded app to release memory or improve performance for the app running in the foreground. -When the user selects the app again, bringing it -back to the foreground, the OS relaunches it. -But, unless you've set up a way to save the -state of the app before it was sent to the background, -you've lost the state and the app starts from -scratch. The user has lost the continuity they expect -and are generally annoyed. (Imagine filling out a -lengthy form and being interrupted by a phone call -before hitting **Submit**...) +You can use the [`RestorationManager`][] (and related) +classes to handle state restoration. +An iOS app requires [a bit of extra setup][] in Xcode, +but the restoration classes otherwise work the same on +both iOS and Android. -So, how can you restore the state of the app so that -it looks like it did before it was sent to the -background? +For more information, check out [State restoration on Android][] +and the [VeggieSeasons][] code sample. -Flutter has a solution for that scenario with the -[`RestorationManager`][] (and related) classes -in the [services][] library. -With the `RestorationManager`, the Flutter Framework -provides the state data to the engine _as the state -changes_, so that the app is ready when the OS signals -that it's about to kill the app. - -The `RestorationManager` manages the restoration -data, which is serialized to a tree of [`RestorationBuckets`[], -that are synchronized with the Flutter engine. - -You must decide what state you want to save and restore, -but you can only save specific types to a `RestorationBucket`, -namely: null, bool, int, double, String, Uint8List -(and other typed data), List, Map, and child `RestorationBuckets`. - -## Enabling state restoration - -Most every Flutter app contains a `main` method that calls the -[`runApp`][] function. You can use `runApp` -to set up the framework and the bindings that bind -the Dart code to the native Flutter engine. -In this case, set up the `RestorationManager` in the -`runApp` function. For example: - -```dart -void main() { - /... - - runApp( - const RootRestorationScope( - restorationId: 'root', - child: myApp(), - ), - ); -} -``` - -[`runApp`]: {{site.api}}/flutter/widgets/runApp.html - -## Add RestorableMixin to the State class - -The easiest way to implement restorable state is to -add the [`RestorableMixin`][] to the app's `State` class. - -```dart -class _MyAppState extends State with RestorationMixin -``` - -Once you've added the restore state mixin, -your IDE asks you to implement an abstract method -called [`restoreState`][]. The `restoreState` method calls -[`registerForRestoration`][] to register a property -that you want to save. For example: - -```dart -@override -void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(_appState, 'state'); -} -``` - -Call `registerForRestoration` once for every -piece of state that you want to save and restore. - ---- -You might also want to check out packages on pub.dev that -help with state restoration, such as [`statePersistence`][]. - -[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html -[`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html -[`statePersistence`]: {{site.pub-pkg}}/state_persistence - -## Testing state restoration - -For advice on testing, see -[Testing state restoration][] on the -[`RestorationManager`][] page. - -[Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration - ---------------------- - -## Enabling state restoration - -To enable state restoration on iOS, -a restoration identifier has to be assigned to the -[`FlutterViewController`][]. -If you are using the standard embedding (produced by flutter create), -use with the following steps: - -1. In the app's directory, open `ios/Runner.xcodeproj` with Xcode. -1. Select `Main.storyboard` under **Runner/Runner** - in the Project Navigator on the left. -1. Select the Flutter View Controller under - **Flutter View Controller Scene** in the view hierarchy. -1. Navigate to the **Identity Inspector** in the panel on the right. -1. Enter a unique restoration ID in the provided field. -1. Save the project. - - -[`FlutterViewController`]: {{site.api}}/objcdoc/Classes/FlutterViewController.html -[`RestorationBuckets`]: {{site.api}}/flutter/services/RestorationBucket-class.html +[a bit of extra setup]: {{site.api}}/flutter/services/RestorationManager-class.html#state-restoration-on-ios [`RestorationManager`]: {{site.api}}/flutter/services/RestorationManager-class.html -[services]: {{site.api}}/flutter/services/services-library.html - +[State restoration on Android]: {{site.url}}/development/platform-integration/android/restore-state-android +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons From e57e9ff2fc73ed63e6c13530931daca2808a0e3c Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Wed, 5 Apr 2023 12:46:04 -0700 Subject: [PATCH 03/10] Incorporating some feedback. --- .../android/restore-state-android.md | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 94c78983989..6ea72eda3d5 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -108,9 +108,17 @@ void main() { } ``` -QUESTION for Michael: I thought that Flutter automatically created - the Scope for every route. But this app manually - creats a Scope for the page. ?? (xxx) +{{site.alert.secondary}} + **Creating the Scope:** + In most cases, specifying a `restorationScopeID` on + [`MaterialApp`][], [`CupertinoApp`][], or [`WidgetsApp`][] + enables state restoration and automatically inserts + a [`RootRestorationScope`][] for you. + However, the VeggieSeasons app has restorable state + that lives _above_ the `CupertinoApp`, + which is why it manually inserts a `RootRestorationScope`. +{{site.alert.end}} + [`runApp`]: {{site.api}}/flutter/widgets/runApp.html @@ -123,9 +131,14 @@ add the [`RestorationMixin`][] to the app's `State` class. class _VeggieAppState extends State with RestorationMixin ``` +You can only restore state with the `RestorationMixin` +that is stored in `RestorableProperty` (and its subclasses). +To restore the state in those properties, +you have to register them in [`restoreState`][]. + Once you've added the restore state mixin, your IDE asks you to implement an abstract method -called [`restoreState`][]. The `restoreState` method calls +called `restoreState`. The `restoreState` method calls [`registerForRestoration`][] to register that you intend to save state. For example: @@ -140,13 +153,19 @@ void restoreState(RestorationBucket? oldBucket, bool initialRestore) { [`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html [`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html -## Step 3: Define a restorationScopeId for each scope +## Step 3: Define restorationScopeId and restorationIDs -Each Scope in the app should have a `restorationScopeId`, -meaning that, at a minimum, you will have one scope per -route. If you create additional scopes -(to avoid `restorationId` duplication, for example), -then also assign those with a unique ID. +Providing a `restorationScopeId` or `restorationId` +to a widget turns on state restoration for that +particular widget: It will now store its own state +under the provided it in the surrounding restoration bucket. +The difference between a `restorationId` and a +`restorationScopeID` is that widgets that take a +`restorationScopeID` create a new `restorationScope` +(a `RestorationBucket`, for example) into which all +children will store their state. A `restorationId` +means the widget (and its children) store the data +in the surrounding bucket. ```dart restorationScopeId: 'app', @@ -168,7 +187,7 @@ For more information on navigation and the [go_router]: {{site.pub}}/packages/go_router [Navigation and routing]: {{site.url}}/development/ui/navigation -## Step 4: Save the state you want to restore +## Step 5: Save the state you want to restore Most of the state restoration work occurs in the `build` method on the `State` class that implements From 7a83520de103ede3b7ede20b93e55926e3807441 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Wed, 19 Apr 2023 15:23:02 -0700 Subject: [PATCH 04/10] Stashing --- .../android/restore-state-android.md | 101 ++++++++++++------ 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 6ea72eda3d5..0492a9900f2 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -13,8 +13,8 @@ When the user selects the app again, bringing it back to the foreground, the OS relaunches it. But, unless you've set up a way to save the state of the app before it was killed, -you've lost the state and the app starts from -scratch. The user has lost the continuity they expect, +you've lost the state and the app starts from scratch. +The user has lost the continuity they expect, which is clearly not ideal. (Imagine filling out a lengthy form and being interrupted by a phone call _before_ clicking **Submit**.) @@ -36,7 +36,7 @@ moments to prepare. Instance state vs long-lived state When should you use the `RestorationManager` and when should you save state to long term storage? - _Instance state_ or + _Instance state_ (also called _short-term_ or _ephemeral_ state), includes unsubmitted form field values, the currently selected tab, and so on. On Android, this is @@ -50,6 +50,43 @@ moments to prepare. [state]: {{site.url}}/development/data-and-backend/state-mgmt/ephemeral-vs-app +## Overview + +Conceptually, +enable state restoration with just a few tasks: + +1. Define a `restorationId` or a `restorationScopeId` + for all widgets that support it, + such as [`TextField`][] and [`ScrollView`][]. + This enables built-in state restoration for those widgets. + +2. For custom widgets, + you must decide what state you want to restore + and hold that state in a [`RestorableProperty`][]. + (We offer various subclasses for different data types.) + Define those `RestorableProperty` widgets + in a `State` class that uses the [`RestorationMixin`][]. + Register those widgets with the mixin in a + `restoreState` method. + +3. Bonus step: If you use the navigator API, + only call methods that have "restorable" in the name. + +Other considerations: + +* If you provide a `restorationId` to + `MaterialApp`, `CupertinoApp`, or `WidgetsApp`, + it enables state restoration by injecting a + `RootRestorationScope`. + If you need to restore state _above_ the app class, + inject a `RootRestorationScope` manually. + +[`RestorableProperty']: {{site.api}}/flutter/widgets/RestorableProperty-class.html +[`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html +[`ScrollView`]: {{site.api}}/flutter/widgets/ScrollView/restorationId.html +[`TextField`]: {{site.api}}/flutter/material/TextField/restorationId.html + +{% comment %} The `RestorationManager` manages the restoration data, which is serialized to a tree of [`RestorationBucket`][], that are synchronized with the Flutter engine. @@ -71,6 +108,7 @@ You must decide what state you want to save and restore, but you can only save specific types to a `RestorationBucket`, namely: `null`, `bool`, `int`, `double`, `String`, `Uint8List` (and other typed data), `List`, `Map`, and child `RestorationBucket`s. +{% endcomment %} {{site.alert.note}} The code in this page was taken from [VeggieSeasons][]. @@ -108,18 +146,6 @@ void main() { } ``` -{{site.alert.secondary}} - **Creating the Scope:** - In most cases, specifying a `restorationScopeID` on - [`MaterialApp`][], [`CupertinoApp`][], or [`WidgetsApp`][] - enables state restoration and automatically inserts - a [`RootRestorationScope`][] for you. - However, the VeggieSeasons app has restorable state - that lives _above_ the `CupertinoApp`, - which is why it manually inserts a `RootRestorationScope`. -{{site.alert.end}} - - [`runApp`]: {{site.api}}/flutter/widgets/runApp.html ## Step 2: Add RestorationMixin to the State class @@ -153,7 +179,18 @@ void restoreState(RestorationBucket? oldBucket, bool initialRestore) { [`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html [`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html -## Step 3: Define restorationScopeId and restorationIDs +## Step 3: Save the state you want to restore + +Most of the state restoration work occurs in the +`build` method on the `State` class that implements +`RestorationMixin`. Your primary task here is to +define a `restorationId` for each piece of state +you want to save. + +In the VeggieSeasons example, most of this work +is implemented in the `lib/screens` classes. + +## Step 4: Define restorationScopeId and restorationIDs Providing a `restorationScopeId` or `restorationId` to a widget turns on state restoration for that @@ -163,15 +200,15 @@ The difference between a `restorationId` and a `restorationScopeID` is that widgets that take a `restorationScopeID` create a new `restorationScope` (a `RestorationBucket`, for example) into which all -children will store their state. A `restorationId` -means the widget (and its children) store the data -in the surrounding bucket. +children of that scope will store their state. +A `restorationId` means the widget (and its children) +store the data in the surrounding bucket. ```dart restorationScopeId: 'app', ``` -## Step 4: Restoring navigation state +## Step 5: Restoring navigation state If you want your app to return to the route (such as a specific tab) that the user was most recently viewing, @@ -181,26 +218,22 @@ This page won't cover navigation, but the accompanying example, VeggieSeasons, implements this feature using the [go_router][] package. +{{site.alert.secondary}} + **Using the navigator API directly:** + If you use the navigator API directly, + you must use the methods with `restorable` + in the name to ensure that your routes are restored, + such as, [`restorablePush`][]. +{{site.alert.end}} + +[`restorablePush`]: {{site.api}}/flutter/widgets/Navigator/restorablePush.html + For more information on navigation and the `go_router` package, check out [Navigation and routing][]. [go_router]: {{site.pub}}/packages/go_router [Navigation and routing]: {{site.url}}/development/ui/navigation -## Step 5: Save the state you want to restore - -Most of the state restoration work occurs in the -`build` method on the `State` class that implements -`RestorationMixin`. Your primary task here is to -define a `restorationId` for each piece of state -you want to save. - -In the VeggieSeasons example, most of this work -is implemented in the `lib/screens` classes. - -QUESTION for Michael: This part of VeggieSeasons is fairly dense. -I'm not sure what is a good code excerpt to include here. (xxx) - ## Testing state restoration To test state restoration, set up your mobile device so that From 0a330162ebdcf0bb676e9d385c7ae28092c07b6b Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Mon, 24 Apr 2023 16:16:55 -0700 Subject: [PATCH 05/10] Reworking the page based on feedback --- .../android/restore-state-android.md | 255 ++++++------------ 1 file changed, 88 insertions(+), 167 deletions(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 0492a9900f2..a47414752d0 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -6,7 +6,7 @@ description: "How to restore the state of your Android app after it's been kille When a user runs a mobile app and then selects another app to run, the first app is moved to the background, or _backgrounded_. The operating system (both iOS and Android) -often kill the backgrounded app to release memory or +typically kill the backgrounded app to release memory and improve performance for the app running in the foreground. When the user selects the app again, bringing it @@ -23,17 +23,17 @@ So, how can you restore the state of the app so that it looks like it did before it was sent to the background? -Flutter has a solution for that scenario with the +Flutter has a solution for this with the [`RestorationManager`][] (and related classes) in the [services][] library. -With the `RestorationManager`, the Flutter Framework +With the `RestorationManager`, the Flutter framework provides the state data to the engine _as the state changes_, so that the app is ready when the OS signals that it's about to kill the app, giving the app only moments to prepare. {{site.alert.secondary}} - Instance state vs long-lived state + **Instance state vs long-lived state** When should you use the `RestorationManager` and when should you save state to long term storage? _Instance state_ @@ -43,196 +43,89 @@ moments to prepare. limited to 1 MB and, if the app exceeds this, it crashes with a `TransactionTooLargeException` error in the native code. - To learn more about short term and long term state, - check out [Differentiate between ephemeral state - and app state][state]. {{site.alert.end}} [state]: {{site.url}}/development/data-and-backend/state-mgmt/ephemeral-vs-app ## Overview -Conceptually, -enable state restoration with just a few tasks: +You can enable state restoration with just a couple tasks: 1. Define a `restorationId` or a `restorationScopeId` for all widgets that support it, such as [`TextField`][] and [`ScrollView`][]. - This enables built-in state restoration for those widgets. + This automatically enables built-in state restoration + for those widgets. 2. For custom widgets, you must decide what state you want to restore and hold that state in a [`RestorableProperty`][]. - (We offer various subclasses for different data types.) + (The Flutter API provides various subclasses for + different data types.) Define those `RestorableProperty` widgets in a `State` class that uses the [`RestorationMixin`][]. Register those widgets with the mixin in a `restoreState` method. -3. Bonus step: If you use the navigator API, - only call methods that have "restorable" in the name. - Other considerations: -* If you provide a `restorationId` to - `MaterialApp`, `CupertinoApp`, or `WidgetsApp`, - it enables state restoration by injecting a - `RootRestorationScope`. +* Providing a `restorationId` to + `MaterialApp`, `CupertinoApp`, or `WidgetsApp` + automatically enables state restoration by + injecting a `RootRestorationScope`. If you need to restore state _above_ the app class, inject a `RootRestorationScope` manually. -[`RestorableProperty']: {{site.api}}/flutter/widgets/RestorableProperty-class.html -[`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html -[`ScrollView`]: {{site.api}}/flutter/widgets/ScrollView/restorationId.html -[`TextField`]: {{site.api}}/flutter/material/TextField/restorationId.html - -{% comment %} -The `RestorationManager` manages the restoration -data, which is serialized to a tree of [`RestorationBucket`][], -that are synchronized with the Flutter engine. -A `RestorationBucket` contains a key/value pair -of state information that you want to save. The key is a -[`restorationId`][] that is unique to that `RestorationScope`. - -Flutter automatically creates a -[`RestorationScope`][] for each route in your -app to track the `RestorationBucket`s in that scope. -In general, you shouldn't need to worry about -this object _unless_ the scope has two (or more) buckets with -the same `restorationId`. If that occurs, -create a new `RestorationScope` to ensure -that each ID is unique to its scope. -Each scope is assigned a unique [`restorationScopeId`][]. - -You must decide what state you want to save and restore, -but you can only save specific types to a `RestorationBucket`, -namely: `null`, `bool`, `int`, `double`, `String`, `Uint8List` -(and other typed data), `List`, `Map`, and child `RestorationBucket`s. -{% endcomment %} - -{{site.alert.note}} - The code in this page was taken from [VeggieSeasons][]. - VeggieSeasons is a sample app written for iOS that uses Cupertino - widgets. An iOS app requires [a bit of extra setup][] in Xcode, but - the restoration classes otherwise work the same on both iOS and Android. -{{site.alert.end}} - +* **The difference between a `restorationId` and + a `restorationScopeId`:** Widgets that take a + `restorationScopeID` create a new `restorationScope` + (a new `RestorationBucket`) into which all children + store their state. A `restorationId` means the widget + (and its children) store the data in the surrounding bucket. + (QUESTION: Axe this?) + +* You can only restore state with the `RestorationMixin` + that is stored in `RestorableProperty` (and its subclasses), + for example, [`restorablePush`][]. + To restore the state in those properties, + you have to register them in `restoreState`. + (QUESTION: Axe this?) + +* Flutter automatically creates a + [`RestorationScope`][] for each route in your + app to track the `RestorationBucket`s in that scope. + In general, you shouldn't need to worry about + this object _unless_ the scope has two (or more) buckets with + the same `restorationId`. If that occurs, + create a new `RestorationScope` to ensure + that each ID is unique to its scope. + (QUESTION: Axe this?) [a bit of extra setup]: {{site.api}}/flutter/services/RestorationManager-class.html#state-restoration-on-ios +[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html [`restorationId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationId.html [`restorationScopeId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationScopeId.html -[`RestorationScope`]: {{site.api}}/flutter/widgets/RestorationScope-class.html -[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons - -## Step 1: Enabling state restoration - -Most every Flutter app contains a `main` method that calls the -[`runApp`][] function. You can use `runApp` -to set up the framework and the bindings that bind -the Dart code to the native Flutter engine. -Set up the `RestorationManager` in the `runApp` function. -For example: - -```dart -void main() { - /... - - runApp( - const RootRestorationScope( - restorationId: 'root', - child: VeggieApp(), - ), - ); -} -``` - -[`runApp`]: {{site.api}}/flutter/widgets/runApp.html - -## Step 2: Add RestorationMixin to the State class - -To implement restorable state, -add the [`RestorationMixin`][] to the app's `State` class. - -```dart -class _VeggieAppState extends State with RestorationMixin -``` - -You can only restore state with the `RestorationMixin` -that is stored in `RestorableProperty` (and its subclasses). -To restore the state in those properties, -you have to register them in [`restoreState`][]. - -Once you've added the restore state mixin, -your IDE asks you to implement an abstract method -called `restoreState`. The `restoreState` method calls -[`registerForRestoration`][] to register that you -intend to save state. For example: - -```dart -@override -void restoreState(RestorationBucket? oldBucket, bool initialRestore) { - registerForRestoration(_appState, 'state'); -} -``` - -[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html [`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html +[`RestorationScope`]: {{site.api}}/flutter/widgets/RestorationScope-class.html [`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons -## Step 3: Save the state you want to restore - -Most of the state restoration work occurs in the -`build` method on the `State` class that implements -`RestorationMixin`. Your primary task here is to -define a `restorationId` for each piece of state -you want to save. - -In the VeggieSeasons example, most of this work -is implemented in the `lib/screens` classes. - -## Step 4: Define restorationScopeId and restorationIDs - -Providing a `restorationScopeId` or `restorationId` -to a widget turns on state restoration for that -particular widget: It will now store its own state -under the provided it in the surrounding restoration bucket. -The difference between a `restorationId` and a -`restorationScopeID` is that widgets that take a -`restorationScopeID` create a new `restorationScope` -(a `RestorationBucket`, for example) into which all -children of that scope will store their state. -A `restorationId` means the widget (and its children) -store the data in the surrounding bucket. - -```dart -restorationScopeId: 'app', -``` +## Restoring navigation state -## Step 5: Restoring navigation state +If you want your app to return to a particular route +(the shopping cart, for example) that the user was most +recently viewing, then you must implement +restoration state for navigation, as well. -If you want your app to return to the route (such -as a specific tab) that the user was most recently viewing, -then you need to implement -restoration state for the navigation, as well. -This page won't cover navigation, but the -accompanying example, VeggieSeasons, implements this -feature using the [go_router][] package. +If you use the navigator API directly, +you must use the methods with `restorable` +in the name to ensure that your routes are restored, +such as [`restorablePush`][]. -{{site.alert.secondary}} - **Using the navigator API directly:** - If you use the navigator API directly, - you must use the methods with `restorable` - in the name to ensure that your routes are restored, - such as, [`restorablePush`][]. -{{site.alert.end}} - -[`restorablePush`]: {{site.api}}/flutter/widgets/Navigator/restorablePush.html - -For more information on navigation and the -`go_router` package, check out [Navigation and routing][]. - -[go_router]: {{site.pub}}/packages/go_router -[Navigation and routing]: {{site.url}}/development/ui/navigation +The VeggieSeasons example (listed under "Other resources" below) +implements navigation with the [`go_router`][] package. +Setting the `restorationId` +values occur in the `lib/screens` classes. ## Testing state restoration @@ -242,14 +135,13 @@ To learn how to do this for both iOS and Android, check out [Testing state restoration][] on the [`RestorationManager`][] page. -{{site.alert.note}} - Don't forget to reenable +{{site.alert.warning}} + **Don't forget to reenable storing state on your device once you are - finished with testing! + finished with testing!** {{site.alert.end}} [Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration - [`FlutterViewController`]: {{site.api}}/objcdoc/Classes/FlutterViewController.html [`RestorationBucket`]: {{site.api}}/flutter/services/RestorationBucket-class.html [`RestorationManager`]: {{site.api}}/flutter/services/RestorationManager-class.html @@ -257,8 +149,37 @@ check out [Testing state restoration][] on the ## Other resources -You might want to check out packages on pub.dev that -perform state restoration, such as [`statePersistence`][]. +For further information on state restoration, +check out the following resources: + +* For an example that implements state restoration, + check out [VeggieSeasons][], a sample app written + for iOS that uses Cupertino widgets. An iOS app requires + [a bit of extra setup][] in Xcode, but the restoration + classes otherwise work the same on both iOS and Android.
+ The following list links to relevant parts of the VeggieSeasons + example: + * [Defining a `RestorablePropery` as an instance property]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L33-L37) + * [Registering the properties]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L49-L54) + * [Updating the properties values]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L108-L109) + * [Using property values in build]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L205-L210)
+ +* To learn more about short term and long term state, + check out [Differentiate between ephemeral state + and app state][state]. -[`statePersistence`]: {{site.pub-pkg}}/state_persistence +* You might want to check out packages on pub.dev that + perform state restoration, such as [`statePersistence`][]. + +* For more information on navigation and the + `go_router` package, check out [Navigation and routing][]. +[`RestorableProperty`]: {{site.api}}/flutter/widgets/RestorableProperty-class.html +[`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html +[`restorablePush`]: {{site.api}}/flutter/widgets/Navigator/restorablePush.html +[`ScrollView`]: {{site.api}}/flutter/widgets/ScrollView/restorationId.html +[`statePersistence`]: {{site.pub-pkg}}/state_persistence +[`TextField`]: {{site.api}}/flutter/material/TextField/restorationId.html +[`restorablePush`]: {{site.api}}/flutter/widgets/Navigator/restorablePush.html +[`go_router`]: {{site.pub}}/packages/go_router +[Navigation and routing]: {{site.url}}/development/ui/navigation From 84fcf89f2db8613ccda0f1f8e270d53ea0b74412 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:00:40 -0700 Subject: [PATCH 06/10] Fixing a broken link --- src/_data/sidenav.yml | 2 +- .../android/restore-state-android.md | 41 ++++++------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/_data/sidenav.yml b/src/_data/sidenav.yml index 62074ccc41b..7063a57b703 100644 --- a/src/_data/sidenav.yml +++ b/src/_data/sidenav.yml @@ -188,7 +188,7 @@ - title: Deprecated Splash Screen API Migration permalink: /development/platform-integration/android/splash-screen-migration - title: Restore state on Android **NEW** - permalink: /development/platform-integration/ios/restore-state-android + permalink: /development/platform-integration/android/restore-state-android - title: Targeting ChromeOS with Android permalink: /development/platform-integration/android/chromeos - title: iOS diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index a47414752d0..419ecc39a9b 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -6,7 +6,7 @@ description: "How to restore the state of your Android app after it's been kille When a user runs a mobile app and then selects another app to run, the first app is moved to the background, or _backgrounded_. The operating system (both iOS and Android) -typically kill the backgrounded app to release memory and +might kill the backgrounded app to release memory and improve performance for the app running in the foreground. When the user selects the app again, bringing it @@ -67,6 +67,11 @@ You can enable state restoration with just a couple tasks: Register those widgets with the mixin in a `restoreState` method. +3. If you use any Navigator API (like `push`, `pushNamed`, and so on) + migrate to the API that has "restorable" in the name + (`restorablePush`, `resstorablePushNamed`, and so on) + to restore the navigation stack. + Other considerations: * Providing a `restorationId` to @@ -82,27 +87,8 @@ Other considerations: (a new `RestorationBucket`) into which all children store their state. A `restorationId` means the widget (and its children) store the data in the surrounding bucket. - (QUESTION: Axe this?) - -* You can only restore state with the `RestorationMixin` - that is stored in `RestorableProperty` (and its subclasses), - for example, [`restorablePush`][]. - To restore the state in those properties, - you have to register them in `restoreState`. - (QUESTION: Axe this?) - -* Flutter automatically creates a - [`RestorationScope`][] for each route in your - app to track the `RestorationBucket`s in that scope. - In general, you shouldn't need to worry about - this object _unless_ the scope has two (or more) buckets with - the same `restorationId`. If that occurs, - create a new `RestorationScope` to ensure - that each ID is unique to its scope. - (QUESTION: Axe this?) [a bit of extra setup]: {{site.api}}/flutter/services/RestorationManager-class.html#state-restoration-on-ios -[`registerForRestoration`]: {{site.api}}/flutter/widgets/RestorationMixin/registerForRestoration.html [`restorationId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationId.html [`restorationScopeId`]: {{site.api}}/flutter/widgets/RestorationScope/restorationScopeId.html [`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html @@ -113,14 +99,14 @@ Other considerations: ## Restoring navigation state If you want your app to return to a particular route -(the shopping cart, for example) that the user was most -recently viewing, then you must implement +that the user was most recently viewing +(the shopping cart, for example), then you must implement restoration state for navigation, as well. -If you use the navigator API directly, -you must use the methods with `restorable` -in the name to ensure that your routes are restored, -such as [`restorablePush`][]. +If you use the Navigator API directly, +migrate the standard methods to restorable +methods (that have "restorable" in the name). +For example, replace `push` with [`restorablePush`][]. The VeggieSeasons example (listed under "Other resources" below) implements navigation with the [`go_router`][] package. @@ -161,7 +147,7 @@ check out the following resources: example: * [Defining a `RestorablePropery` as an instance property]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L33-L37) * [Registering the properties]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L49-L54) - * [Updating the properties values]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L108-L109) + * [Updating the property values]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L108-L109) * [Using property values in build]({{site.github}}/flutter/samples/blob/604c82cd7c9c7807ff6c5ca96fbb01d44a4f2c41/veggieseasons/lib/widgets/trivia.dart#L205-L210)
* To learn more about short term and long term state, @@ -175,7 +161,6 @@ check out the following resources: `go_router` package, check out [Navigation and routing][]. [`RestorableProperty`]: {{site.api}}/flutter/widgets/RestorableProperty-class.html -[`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html [`restorablePush`]: {{site.api}}/flutter/widgets/Navigator/restorablePush.html [`ScrollView`]: {{site.api}}/flutter/widgets/ScrollView/restorationId.html [`statePersistence`]: {{site.pub-pkg}}/state_persistence From d13e4f619dd552ee7d6fc47fdd906a4d5beafd59 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:12:51 -0700 Subject: [PATCH 07/10] Fix typo --- .../platform-integration/android/restore-state-android.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 419ecc39a9b..1346a1d95be 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -49,7 +49,7 @@ moments to prepare. ## Overview -You can enable state restoration with just a couple tasks: +You can enable state restoration with just a few tasks: 1. Define a `restorationId` or a `restorationScopeId` for all widgets that support it, From 2d14d2512f8d3d79fa396a583eb5c91b6ece67ce Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:29:14 -0700 Subject: [PATCH 08/10] Update src/development/platform-integration/android/restore-state-android.md Co-authored-by: Parker Lougheed --- .../platform-integration/android/restore-state-android.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index 1346a1d95be..e8e3d14db26 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -94,7 +94,7 @@ Other considerations: [`RestorationMixin`]: {{site.api}}/flutter/widgets/RestorationMixin-mixin.html [`RestorationScope`]: {{site.api}}/flutter/widgets/RestorationScope-class.html [`restoreState`]: {{site.api}}/flutter/widgets/RestorationMixin/restoreState.html -[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/main/veggieseasons ## Restoring navigation state From c5f6e3443f44dcb5303b460bdb9d49a3a5ac38b7 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:29:31 -0700 Subject: [PATCH 09/10] Update src/development/platform-integration/ios/restore-state-ios.md Co-authored-by: Parker Lougheed --- src/development/platform-integration/ios/restore-state-ios.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/development/platform-integration/ios/restore-state-ios.md b/src/development/platform-integration/ios/restore-state-ios.md index 89b5f78f17a..22d72ffa6bc 100644 --- a/src/development/platform-integration/ios/restore-state-ios.md +++ b/src/development/platform-integration/ios/restore-state-ios.md @@ -21,5 +21,5 @@ and the [VeggieSeasons][] code sample. [a bit of extra setup]: {{site.api}}/flutter/services/RestorationManager-class.html#state-restoration-on-ios [`RestorationManager`]: {{site.api}}/flutter/services/RestorationManager-class.html [State restoration on Android]: {{site.url}}/development/platform-integration/android/restore-state-android -[VeggieSeasons]: {{site.github}}/flutter/samples/tree/master/veggieseasons +[VeggieSeasons]: {{site.github}}/flutter/samples/tree/main/veggieseasons From d927ca453015f1325d814b0932ed3fd6b81d3806 Mon Sep 17 00:00:00 2001 From: "Shams Zakhour (ignore Sfshaza)" <44418985+sfshaza2@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:29:57 -0700 Subject: [PATCH 10/10] Update src/development/platform-integration/android/restore-state-android.md Co-authored-by: Parker Lougheed --- .../platform-integration/android/restore-state-android.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/development/platform-integration/android/restore-state-android.md b/src/development/platform-integration/android/restore-state-android.md index e8e3d14db26..e19f39455fb 100644 --- a/src/development/platform-integration/android/restore-state-android.md +++ b/src/development/platform-integration/android/restore-state-android.md @@ -122,9 +122,9 @@ check out [Testing state restoration][] on the [`RestorationManager`][] page. {{site.alert.warning}} - **Don't forget to reenable + Don't forget to reenable storing state on your device once you are - finished with testing!** + finished with testing! {{site.alert.end}} [Testing state restoration]: {{site.api}}/flutter/services/RestorationManager-class.html#testing-state-restoration