// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'basic.dart'; import 'framework.dart'; export 'package:flutter/services.dart' show RestorationBucket; /// Creates a new scope for restoration IDs used by descendant widgets to claim /// [RestorationBucket]s. /// /// {@template flutter.widgets.restoration.scope} /// A restoration scope inserts a [RestorationBucket] into the widget tree, /// which descendant widgets can access via [RestorationScope.of]. It is /// uncommon for descendants to directly store data in this bucket. Instead, /// descendant widgets should consider storing their own restoration data in a /// child bucket claimed with [RestorationBucket.claimChild] from the bucket /// provided by this scope. /// {@endtemplate} /// /// The bucket inserted into the widget tree by this scope has been claimed from /// the surrounding [RestorationScope] using the provided [restorationId]. If /// the [RestorationScope] is moved to a different part of the widget tree under /// a different [RestorationScope], the bucket owned by this scope with all its /// children and the data contained in them is moved to the new scope as well. /// /// This widget will not make a [RestorationBucket] available to descendants if /// [restorationId] is null or when there is no surrounding restoration scope to /// claim a bucket from. In this case, descendant widgets invoking /// [RestorationScope.of] will receive null as a return value indicating that no /// bucket is available for storing restoration data. This will turn off state /// restoration for the widget subtree. /// /// See also: /// /// * [RootRestorationScope], which inserts the root bucket provided by /// the [RestorationManager] into the widget tree and makes it accessible /// for descendants via [RestorationScope.of]. /// * [UnmanagedRestorationScope], which inserts a provided [RestorationBucket] /// into the widget tree and makes it accessible for descendants via /// [RestorationScope.of]. /// * [RestorationMixin], which may be used in [State] objects to manage the /// restoration data of a [StatefulWidget] instead of manually interacting /// with [RestorationScope]s and [RestorationBucket]s. /// * [RestorationManager], which describes the basic concepts of state /// restoration in Flutter. class RestorationScope extends StatefulWidget { /// Creates a [RestorationScope]. /// /// Providing null as the [restorationId] turns off state restoration for /// the [child] and its descendants. /// /// The [child] must not be null. const RestorationScope({ Key key, @required this.restorationId, @required this.child, }) : assert(child != null), super(key: key); /// Returns the [RestorationBucket] inserted into the widget tree by the /// closest ancestor [RestorationScope] of `context`. /// /// To avoid accidentally overwriting data already stored in the bucket by its /// owner, data should not be stored directly in the bucket returned by this /// method. Instead, consider claiming a child bucket from the returned bucket /// (via [RestorationBucket.claimChild]) and store the restoration data in /// that child. /// /// This method returns null if state restoration is turned off for this /// subtree. static RestorationBucket of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<UnmanagedRestorationScope>()?.bucket; } /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.child} final Widget child; /// The restoration ID used by this widget to obtain a child bucket from the /// surrounding [RestorationScope]. /// /// The child bucket obtained from the surrounding scope is made available to /// descendant widgets via [RestorationScope.of]. /// /// If this is null, [RestorationScope.of] invoked by descendants will return /// null which effectively turns off state restoration for this subtree. final String restorationId; @override State<RestorationScope> createState() => _RestorationScopeState(); } class _RestorationScopeState extends State<RestorationScope> with RestorationMixin { @override String get restorationId => widget.restorationId; @override void restoreState(RestorationBucket oldBucket, bool initialRestore) { // Nothing to do. // The bucket gets injected into the widget tree in the build method. } @override Widget build(BuildContext context) { return UnmanagedRestorationScope( bucket: bucket, // `bucket` is provided by the RestorationMixin. child: widget.child, ); } } /// Inserts a provided [RestorationBucket] into the widget tree and makes it /// available to descendants via [RestorationScope.of]. /// /// {@macro flutter.widgets.restoration.scope} /// /// If [bucket] is null, no restoration bucket is made available to descendant /// widgets ([RestorationScope.of] invoked from a descendant will return null). /// This effectively turns off state restoration for the subtree because no /// bucket for storing restoration data is made available. /// /// See also: /// /// * [RestorationScope], which inserts a bucket obtained from a surrounding /// restoration scope into the widget tree and makes it accessible /// for descendants via [RestorationScope.of]. /// * [RootRestorationScope], which inserts the root bucket provided by /// the [RestorationManager] into the widget tree and makes it accessible /// for descendants via [RestorationScope.of]. /// * [RestorationMixin], which may be used in [State] objects to manage the /// restoration data of a [StatefulWidget] instead of manually interacting /// with [RestorationScope]s and [RestorationBucket]s. /// * [RestorationManager], which describes the basic concepts of state /// restoration in Flutter. class UnmanagedRestorationScope extends InheritedWidget { /// Creates an [UnmanagedRestorationScope]. /// /// When [bucket] is null state restoration is turned off for the [child] and /// its descendants. /// /// The [child] must not be null. const UnmanagedRestorationScope({ Key key, this.bucket, @required Widget child, }) : assert(child != null), super(key: key, child: child); /// The [RestorationBucket] that this widget will insert into the widget tree. /// /// Descendant widgets may obtain this bucket via [RestorationScope.of]. final RestorationBucket bucket; @override bool updateShouldNotify(UnmanagedRestorationScope oldWidget) { return oldWidget.bucket != bucket; } } /// Inserts a child bucket of [RestorationManager.rootBucket] into the widget /// tree and makes it available to descendants via [RestorationScope.of]. /// /// This widget is usually used near the root of the widget tree to enable the /// state restoration functionality for the application. For all other use /// cases, consider using a regular [RestorationScope] instead. /// /// The root restoration bucket can only be retrieved asynchronously from the /// [RestorationManager]. To ensure that the provided [child] has its /// restoration data available the first time it builds, the /// [RootRestorationScope] will build an empty [Container] instead of the actual /// [child] until the root bucket is available. To hide the empty container from /// the eyes of users, the [RootRestorationScope] also delays rendering the /// first frame while the container is shown. On platforms that show a splash /// screen on app launch the splash screen is kept up (hiding the empty /// container) until the bucket is available and the [child] is ready to be /// build. /// /// The exact behavior of this widget depends on its ancestors: When the /// [RootRestorationScope] does not find an ancestor restoration bucket via /// [RestorationScope.of] it will claim a child bucket from the root restoration /// bucket ([RestorationManager.rootBucket]) using the provided [restorationId] /// and inserts that bucket into the widget tree where descendants may access it /// via [RestorationScope.of]. If the [RootRestorationScope] finds a non-null /// ancestor restoration bucket via [RestorationScope.of] it will behave like a /// regular [RestorationScope] instead: It will claim a child bucket from that /// ancestor and insert that child into the widget tree. /// /// Unlike the [RestorationScope] widget, the [RootRestorationScope] will /// guarantee that descendants have a bucket available for storing restoration /// data as long as [restorationId] is not null and [RestorationManager] is /// able to provide a root bucket. In other words, it will force-enable /// state restoration for the subtree if [restorationId] is not null. /// /// If [restorationId] is null, no bucket is made available to descendants, /// which effectively turns off state restoration for this subtree. /// /// See also: /// /// * [RestorationScope], which inserts a bucket obtained from a surrounding /// restoration scope into the widget tree and makes it accessible /// for descendants via [RestorationScope.of]. /// * [UnmanagedRestorationScope], which inserts a provided [RestorationBucket] /// into the widget tree and makes it accessible for descendants via /// [RestorationScope.of]. /// * [RestorationMixin], which may be used in [State] objects to manage the /// restoration data of a [StatefulWidget] instead of manually interacting /// with [RestorationScope]s and [RestorationBucket]s. /// * [RestorationManager], which describes the basic concepts of state /// restoration in Flutter. class RootRestorationScope extends StatefulWidget { /// Creates a [RootRestorationScope]. /// /// Providing null as the [restorationId] turns off state restoration for /// the [child] and its descendants. /// /// The [child] must not be null. const RootRestorationScope({ Key key, @required this.restorationId, @required this.child, }) : assert(child != null), super(key: key); /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.child} final Widget child; /// The restoration ID used to identify the child bucket that this widget /// will insert into the tree. /// /// If this is null, no bucket is made available to descendants and state /// restoration for the subtree is essentially turned off. final String restorationId; @override State<RootRestorationScope> createState() => _RootRestorationScopeState(); } class _RootRestorationScopeState extends State<RootRestorationScope> { bool _okToRenderBlankContainer; bool _rootBucketValid = false; RestorationBucket _rootBucket; RestorationBucket _ancestorBucket; @override void didChangeDependencies() { super.didChangeDependencies(); _ancestorBucket = RestorationScope.of(context); _loadRootBucketIfNecessary(); _okToRenderBlankContainer ??= widget.restorationId != null && _needsRootBucketInserted; } @override void didUpdateWidget(RootRestorationScope oldWidget) { super.didUpdateWidget(oldWidget); _loadRootBucketIfNecessary(); } bool get _needsRootBucketInserted => _ancestorBucket == null; bool get _isWaitingForRootBucket { return widget.restorationId != null && _needsRootBucketInserted && !_rootBucketValid; } bool _isLoadingRootBucket = false; void _loadRootBucketIfNecessary() { if (_isWaitingForRootBucket && !_isLoadingRootBucket) { _isLoadingRootBucket = true; RendererBinding.instance.deferFirstFrame(); ServicesBinding.instance.restorationManager.rootBucket.then((RestorationBucket bucket) { _isLoadingRootBucket = false; if (mounted) { ServicesBinding.instance.restorationManager.addListener(_replaceRootBucket); setState(() { _rootBucket = bucket; _rootBucketValid = true; _okToRenderBlankContainer = false; }); } RendererBinding.instance.allowFirstFrame(); }); } } void _replaceRootBucket() { _rootBucketValid = false; _rootBucket = null; ServicesBinding.instance.restorationManager.removeListener(_replaceRootBucket); _loadRootBucketIfNecessary(); assert(!_isWaitingForRootBucket); // Ensure that load finished synchronously. } @override void dispose() { ServicesBinding.instance.restorationManager.removeListener(_replaceRootBucket); super.dispose(); } @override Widget build(BuildContext context) { if (_okToRenderBlankContainer && _isWaitingForRootBucket) { return const SizedBox.shrink(); } return UnmanagedRestorationScope( bucket: _ancestorBucket ?? _rootBucket, child: RestorationScope( restorationId: widget.restorationId, child: widget.child, ), ); } } /// Manages an object of type `T`, whose value a [State] object wants to have /// restored during state restoration. /// /// The property wraps an object of type `T`. It knows how to store its value in /// the restoration data and it knows how to re-instantiate that object from the /// information it previously stored in the restoration data. /// /// The knowledge of how to store the wrapped object in the restoration data is /// encoded in the [toPrimitives] method and the knowledge of how to /// re-instantiate the object from that data is encoded in the [fromPrimitives] /// method. A call to [toPrimitives] must return a representation of the wrapped /// object that can be serialized with the [StandardMessageCodec]. If any /// collections (e.g. [List]s, [Map]s, etc.) are returned, they must not be /// modified after they have been returned from [toPrimitives]. At a later point /// in time (which may be after the application restarted), the data obtained /// from [toPrimitives] may be handed back to the property's [fromPrimitives] /// method to restore it to the previous state described by that data. /// /// A [RestorableProperty] needs to be registered to a [RestorationMixin] using /// a restoration ID that is unique within the mixin. The [RestorationMixin] /// provides and manages the [RestorationBucket], in which the data returned by /// [toPrimitives] is stored. /// /// Whenever the value returned by [toPrimitives] (or the [enabled] getter) /// changes, the [RestorableProperty] must call [notifyListeners]. This will /// trigger the [RestorationMixin] to update the data it has stored for the /// property in its [RestorationBucket] to the latest information returned by /// [toPrimitives]. /// /// When the property is registered with the [RestorationMixin], the mixin /// checks whether there is any restoration data available for the property. If /// data is available, the mixin calls [fromPrimitives] on the property, which /// must return an object that matches the object the property wrapped when the /// provided restoration data was obtained from [toPrimitives]. If no /// restoration data is available to restore the property's wrapped object from, /// the mixin calls [createDefaultValue]. The value returned by either of those /// methods is then handed to the property's [initWithValue] method. /// /// Usually, subclasses of [RestorableProperty] hold on to the value provided to /// them in [initWithValue] and make it accessible to the [State] object that /// owns the property. This [RestorableProperty] base class, however, has no /// opinion about what to do with the value provided to [initWithValue]. /// /// The [RestorationMixin] may call [fromPrimitives]/[createDefaultValue] /// followed by [initWithValue] multiple times throughout the life of a /// [RestorableProperty]: Whenever new restoration data is made available to the /// [RestorationMixin] the property is registered with, the cycle of calling /// [fromPrimitives] (if the new restoration data contains information to /// restore the property from) or [createDefaultValue] (if no information for /// the property is available in the new restoration data) followed by a call to /// [initWithValue] repeats. Whenever [initWithValue] is called, the property /// should forget the old value it was wrapping and re-initialize itself with /// the newly provided value. /// /// In a typical use case, a subclass of [RestorableProperty] is instantiated /// either to initialize a member variable of a [State] object or within /// [State.initState]. It is then registered to a [RestorationMixin] in /// [RestorationMixin.restoreState] and later [dispose]ed in [State.dispose]. /// For less common use cases (e.g. if the value stored in a /// [RestorableProperty] is only needed while the [State] object is in a certain /// state), a [RestorableProperty] may be registered with a [RestorationMixin] /// any time after [RestorationMixin.restoreState] has been called for the first /// time. A [RestorableProperty] may also be unregistered from a /// [RestorationMixin] before the owning [State] object is disposed by calling /// [RestorationMixin.unregisterFromRestoration]. This is uncommon, though, and /// will delete the information that the property contributed from the /// restoration data (meaning the value of the property will no longer be /// restored during a future state restoration). /// /// See also: /// /// * [RestorableValue], which is a [RestorableProperty] that makes the wrapped /// value accessible to the owning [State] object via a `value` /// getter and setter. /// * [RestorationMixin], to which a [RestorableProperty] must be registered. /// * [RestorationManager], which describes how state restoration works in /// Flutter. abstract class RestorableProperty<T> extends ChangeNotifier { /// Called by the [RestorationMixin] if no restoration data is available to /// restore the value of the property from to obtain the default value for the /// property. /// /// The method returns the default value that the property should wrap if no /// restoration data is available. After this is called, [initWithValue] will /// be called with this method's return value. /// /// The method may be called multiple times throughout the life of the /// [RestorableProperty]. Whenever new restoration data has been provided to /// the [RestorationMixin] the property is registered to, either this method /// or [fromPrimitives] is called before [initWithValue] is invoked. T createDefaultValue(); /// Called by the [RestorationMixin] to convert the `data` previously /// retrieved from [toPrimitives] back into an object of type `T` that this /// property should wrap. /// /// The object returned by this method is passed to [initWithValue] to restore /// the value that this property is wrapping to the value described by the /// provided `data`. /// /// The method may be called multiple times throughout the life of the /// [RestorableProperty]. Whenever new restoration data has been provided to /// the [RestorationMixin] the property is registered to, either this method /// or [createDefaultValue] is called before [initWithValue] is invoked. T fromPrimitives(Object data); /// Called by the [RestorationMixin] with the `value` returned by either /// [createDefaultValue] or [fromPrimitives] to set the value that this /// property currently wraps. /// /// The [initWithValue] method may be called multiple times throughout the /// life of the [RestorableProperty] whenever new restoration data has been /// provided to the [RestorationMixin] the property is registered to. When /// [initWithValue] is called, the property should forget its previous value /// and re-initialize itself to the newly provided `value`. void initWithValue(T value); /// Called by the [RestorationMixin] to retrieve the information that this /// property wants to store in the restoration data. /// /// The returned object must be serializable with the [StandardMessageCodec] /// and if it includes any collections, those should not be modified after /// they have been returned by this method. /// /// The information returned by this method may be handed back to the property /// in a call to [fromPrimitives] at a later point in time (possibly after the /// application restarted) to restore the value that the property is currently /// wrapping. /// /// When the value returned by this method changes, the property must call /// [notifyListeners]. The [RestorationMixin] will invoke this method whenever /// the property's listeners are notified. Object toPrimitives(); /// Whether the object currently returned by [toPrimitives] should be included /// in the restoration state. /// /// When this returns false, no information is included in the restoration /// data for this property and the property will be initialized to its default /// value (obtained from [createDefaultValue]) the next time that restoration /// data is used for state restoration. /// /// Whenever the value returned by this getter changes, [notifyListeners] must /// be called. When the value changes from true to false, the information last /// retrieved from [toPrimitives] is removed from the restoration data. When /// it changes from false to true, [toPrimitives] is invoked to add the latest /// restoration information provided by this property to the restoration data. bool get enabled => true; bool _disposed = false; @override void dispose() { assert(_debugAssertNotDisposed()); _owner?._unregister(this); super.dispose(); _disposed = true; } // ID under which the property has been registered with the RestorationMixin. String _restorationId; RestorationMixin _owner; void _register(String restorationId, RestorationMixin owner) { assert(_debugAssertNotDisposed()); assert(restorationId != null); assert(owner != null); _restorationId = restorationId; _owner = owner; } void _unregister() { assert(_debugAssertNotDisposed()); assert(_restorationId != null); assert(_owner != null); _restorationId = null; _owner = null; } /// The [State] object that this property is registered with. @protected State get state { assert(_debugAssertNotDisposed()); return _owner; } /// Whether this property is currently registered with a [RestorationMixin]. @protected bool get isRegistered { assert(_debugAssertNotDisposed()); return _restorationId != null; } bool _debugAssertNotDisposed() { assert(() { if (_disposed) { throw FlutterError( 'A $runtimeType was used after being disposed.\n' 'Once you have called dispose() on a $runtimeType, it can no longer be used.' ); } return true; }()); return true; } } /// Manages the restoration data for a [State] object of a [StatefulWidget]. /// /// Restoration data can be serialized out and, at a later point in time, be /// used to restore the stateful members in the [State] object to the same /// values they had when the data was generated. /// /// This mixin organizes the restoration data of a [State] object in /// [RestorableProperty]. All the information that the [State] object wants to /// get restored during state restoration need to be saved in a subclass of /// [RestorableProperty]. For example, to restore the count value in a counter /// app, that value should be stored in a member variable of type /// [RestorableInt] instead of a plain member variable of type [int]. /// /// The mixin ensures that the current values of the [RestorableProperty]s are /// serialized as part of the restoration state. It is up to the [State] to /// ensure that the data stored in the properties is always up to date. When the /// widget is restored from previously generated restoration data, the values of /// the [RestorableProperty]s are automatically restored to the values that had /// when the restoration data was serialized out. /// /// Within a [State] that uses this mixin, [RestorableProperty]s are usually /// instantiated to initialize member variables. Users of the mixin must /// override [restoreState] and register their previously instantiated /// [RestorableProperty]s in this method by calling [registerForRestoration]. /// The mixin calls this method for the first time right after /// [State.initState]. After registration, the values stored in the property /// have either been restored to their previous value or - if no restoration /// data for restoring is available - they are initialized with a /// property-specific default value. At the end of a [State] object's life /// cycle, all restorable properties must be disposed in [State.dispose]. /// /// In addition to being invoked right after [State.initState], [restoreState] /// is invoked again when new restoration data has been provided to the mixin. /// When this happens, the [State] object must re-register all properties with /// [registerForRestoration] again to restore them to their previous values as /// described by the new restoration data. All initialization logic that depends /// on the current value of a restorable property should be included in the /// [restoreState] method to ensure it re-executes when the properties are /// restored to a different value during the life time of the [State] object. /// /// Internally, the mixin stores the restoration data from all registered /// properties in a [RestorationBucket] claimed from the surrounding /// [RestorationScope] using the [State]-provided [restorationId]. The /// [restorationId] must be unique in the surrounding [RestorationScope]. State /// restoration is disabled for the [State] object using this mixin if /// [restorationId] is null or when there is no surrounding [RestorationScope]. /// In that case, the values of the registered properties will not be restored /// during state restoration. /// /// The [RestorationBucket] used to store the registered properties is available /// via the [bucket] getter. Interacting directly with the bucket is uncommon, /// but the [State] object may make this bucket available for its descendants to /// claim child buckets from. For that, the [bucket] is injected into the widget /// tree in [State.build] with the help of an [UnmanagedRestorationScope]. /// /// The [bucket] getter returns null if state restoration is turned off. If /// state restoration is turned on or off during the lifetime of the widget /// (e.g. because [restorationId] changes from null to non-null) the value /// returned by the getter will also change from null to non-null or vice versa. /// The mixin calls [didToggleBucket] on itself to notify the [State] object /// about this change. Overriding this method is not necessary as long as the /// [State] object does not directly interact with the [bucket]. /// /// Whenever the value returned by [restorationId] changes, /// [didUpdateRestorationId] must be called (unless the change already triggers /// a call to [didUpdateWidget]). /// /// {@tool dartpad --template=freeform} /// This example demonstrates how to make a simple counter app restorable by /// using the [RestorationMixin] and a [RestorableInt]. /// /// ```dart imports /// import 'package:flutter/material.dart'; /// ``` /// /// ```dart main /// void main() => runApp(RestorationExampleApp()); /// ``` /// /// ```dart preamble /// class RestorationExampleApp extends StatelessWidget { /// @override /// Widget build(BuildContext context) { /// // The [RootRestorationScope] can be removed once it is part of [MaterialApp]. /// return RootRestorationScope( /// restorationId: 'root', /// child: MaterialApp( /// title: 'Restorable Counter', /// home: RestorableCounter(restorationId: 'counter'), /// ), /// ); /// } /// } /// ``` /// /// ```dart /// class RestorableCounter extends StatefulWidget { /// RestorableCounter({Key key, this.restorationId}) : super(key: key); /// /// final String restorationId; /// /// @override /// _RestorableCounterState createState() => _RestorableCounterState(); /// } /// /// // The [State] object uses the [RestorationMixin] to make the current value /// // of the counter restorable. /// class _RestorableCounterState extends State<RestorableCounter> with RestorationMixin { /// // The current value of the counter is stored in a [RestorableProperty]. /// // During state restoration it is automatically restored to its old value. /// // If no restoration data is available to restore the counter from, it is /// // initialized to the specified default value of zero. /// RestorableInt _counter = RestorableInt(0); /// /// // In this example, the restoration ID for the mixin is passed in through /// // the [StatefulWidget]'s constructor. /// @override /// String get restorationId => widget.restorationId; /// /// @override /// void restoreState(RestorationBucket oldBucket, bool initialRestore) { /// // All restorable properties must be registered with the mixin. After /// // registration, the counter either has its old value restored or is /// // initialized to its default value. /// registerForRestoration(_counter, 'count'); /// } /// /// void _incrementCounter() { /// setState(() { /// // The current value of the property can be accessed and modified via /// // the value getter and setter. /// _counter.value++; /// }); /// } /// /// @override /// void dispose() { /// _counter.dispose(); /// super.dispose(); /// } /// /// @override /// Widget build(BuildContext context) { /// return Scaffold( /// appBar: AppBar( /// title: Text('Restorable Counter'), /// ), /// body: Center( /// child: Column( /// mainAxisAlignment: MainAxisAlignment.center, /// children: <Widget>[ /// Text( /// 'You have pushed the button this many times:', /// ), /// Text( /// '${_counter.value}', /// style: Theme.of(context).textTheme.headline4, /// ), /// ], /// ), /// ), /// floatingActionButton: FloatingActionButton( /// onPressed: _incrementCounter, /// tooltip: 'Increment', /// child: Icon(Icons.add), /// ), /// ); /// } /// } /// ``` /// {@end-tool} /// /// See also: /// /// * [RestorableProperty], which is the base class for all restoration /// properties managed by this mixin. /// * [RestorationManager], which describes how state restoration in Flutter /// works. /// * [RestorationScope], which creates a new namespace for restoration IDs /// in the widget tree. @optionalTypeArgs mixin RestorationMixin<S extends StatefulWidget> on State<S> { /// The restoration ID used for the [RestorationBucket] in which the mixin /// will store the restoration data of all registered properties. /// /// The restoration ID is used to claim a child [RestorationScope] from the /// surrounding [RestorationScope] (accessed via [RestorationScope.of]) and /// the ID must be unique in that scope (otherwise an exception is triggered /// in debug move). /// /// State restoration for this mixin is turned off when this getter returns /// null or when there is no surrounding [RestorationScope] available. When /// state restoration is turned off, the values of the registered properties /// cannot be restored. /// /// Whenever the value returned by this getter changes, /// [didUpdateRestorationId] must be called unless the (unless the change /// already triggered a call to [didUpdateWidget]). /// /// The restoration ID returned by this getter is often provided in the /// constructor of the [StatefulWidget] that this [State] object is associated /// with. @protected String get restorationId; /// The [RestorationBucket] used for the restoration data of the /// [RestorableProperty]s registered to this mixin. /// /// The bucket has been claimed from the surrounding [RestorationScope] using /// [restorationId]. /// /// The getter returns null if state restoration is turned off. When state /// restoration is turned on or off during the lifetime of this mixin (and /// hence the return value of this getter switches between null and non-null) /// [didToggleBucket] is called. /// /// Interacting directly with this bucket is uncommon. However, the bucket may /// be injected into the widget tree in the [State]'s `build` method using an /// [UnmanagedRestorationScope]. That allows descendants to claim child /// buckets from this bucket for their own restoration needs. RestorationBucket get bucket => _bucket; RestorationBucket _bucket; /// Called to initialize or restore the [RestorableProperty]s used by the /// [State] object. /// /// This method is always invoked at least once right after [State.initState] /// to register the [RestorableProperty]s with the mixin even when state /// restoration is turned off or no restoration data is available for this /// [State] object. /// /// Typically, [registerForRestoration] is called from this method to register /// all [RestorableProperty]s used by the [State] object with the mixin. The /// registration will either restore the property's value to the value /// described by the restoration data, if available, or, if no restoration /// data is available - initialize it to a property-specific default value. /// /// The method is called again whenever new restoration data (in the form of a /// new [bucket]) has been provided to the mixin. When that happens, the /// [State] object must re-register all previously registered properties, /// which will restore their values to the value described by the new /// restoration data. /// /// Since the method may change the value of the registered properties when /// new restoration state is provided, all initialization logic that depends /// on a specific value of a [RestorableProperty] should be included in this /// method. That way, that logic re-executes when the [RestorableProperty]s /// have their values restored from newly provided restoration data. /// /// The first time the method is invoked, the provided `oldBucket` argument is /// always null. In subsequent calls triggered by new restoration data in the /// form of a new bucket, the argument given is the previous value of /// [bucket]. @mustCallSuper @protected void restoreState(RestorationBucket oldBucket, bool initialRestore); /// Called when [bucket] switches between null and non-null values. /// /// [State] objects that wish to directly interact with the bucket may /// override this method to store additional values in the bucket when one /// becomes available or to save values stored in a bucket elsewhere when the /// bucket goes away. This is uncommon and storing those values in /// [RestorableProperty]s should be considered instead. /// /// The `oldBucket` is provided to the method when the [bucket] getter changes /// from non-null to null. The `oldBucket` argument is null when the [bucket] /// changes from null to non-null. /// /// See also: /// /// * [restoreState], which is called when the [bucket] changes from one /// non-null value to another non-null value. @mustCallSuper @protected void didToggleBucket(RestorationBucket oldBucket) { // When a bucket is replaced, must `restoreState` is called instead. assert(_bucket?.isReplacing != true); } // Maps properties to their listeners. final Map<RestorableProperty<Object>, VoidCallback> _properties = <RestorableProperty<Object>, VoidCallback>{}; /// Registers a [RestorableProperty] for state restoration. /// /// The registration associates the provided `property` with the provided /// `restorationId`. If restoration data is available for the provided /// `restorationId`, the property's value is restored to the value described /// by the restoration data. If no restoration data is available, the property /// will be initialized to a property-specific default value. /// /// Each property within a [State] object must be registered under a unique /// ID. Only registered properties will have their values restored during /// state restoration. /// /// Typically, this method is called from within [restoreState] to register /// all restorable properties of the owning [State] object. However, if a /// given [RestorableProperty] is only needed when certain conditions are met /// within the [State], [registerForRestoration] may also be called at any /// time after [restoreState] has been invoked for the first time. /// /// A property that has been registered outside of [restoreState] must be /// re-registered within [restoreState] the next time that method is called /// unless it has been unregistered with [unregisterFromRestoration]. @protected void registerForRestoration(RestorableProperty<Object> property, String restorationId) { assert(property != null); assert(restorationId != null); assert(property._restorationId == null || (_debugDoingRestore && property._restorationId == restorationId), 'Property is already registered under ${property._restorationId}.', ); assert(_debugDoingRestore || !_properties.keys.map((RestorableProperty<Object> r) => r._restorationId).contains(restorationId), '"$restorationId" is already registered to another property.' ); final bool hasSerializedValue = bucket?.contains(restorationId) == true; final Object initialValue = hasSerializedValue ? property.fromPrimitives(bucket.read<Object>(restorationId)) : property.createDefaultValue(); if (!property.isRegistered) { property._register(restorationId, this); final VoidCallback listener = () { if (bucket == null) return; _updateProperty(property); }; property.addListener(listener); _properties[property] = listener; } assert( property._restorationId == restorationId && property._owner == this && _properties.containsKey(property) ); property.initWithValue(initialValue); if (!hasSerializedValue && property.enabled && bucket != null) { _updateProperty(property); } assert(() { _debugPropertiesWaitingForReregistration?.remove(property); return true; }()); } /// Unregisters a [RestorableProperty] from state restoration. /// /// The value of the `property` is removed from the restoration data and it /// will not be restored if that data is used in a future state restoration. /// /// Calling this method is uncommon, but may be necessary if the data of a /// [RestorableProperty] is only relevant when the [State] object is in a /// certain state. When the data of a property is no longer necessary to /// restore the internal state of a [State] object, it may be removed from the /// restoration data by calling this method. @protected void unregisterFromRestoration(RestorableProperty<Object> property) { assert(property != null); assert(property._owner == this); _bucket?.remove<Object>(property._restorationId); _unregister(property); } /// Must be called when the value returned by [restorationId] changes. /// /// This method is automatically called from [didUpdateWidget]. Therefore, /// manually invoking this method may be omitted when the change in /// [restorationId] was caused by an updated widget. @protected void didUpdateRestorationId() { // There's nothing to do if: // - We don't have a parent to claim a bucket from. // - Our current bucket already uses the provided restoration ID. // - There's a restore pending, which means that didUpdateDependencies // will be called and we handle the rename there. if (_currentParent == null || _bucket?.restorationId == restorationId || restorePending) { return; } final RestorationBucket oldBucket = _bucket; assert(!restorePending); final bool didReplaceBucket = _updateBucketIfNecessary(parent: _currentParent, restorePending: false); if (didReplaceBucket) { assert(oldBucket != _bucket); assert(_bucket == null || oldBucket == null); oldBucket?.dispose(); } } @override void didUpdateWidget(S oldWidget) { super.didUpdateWidget(oldWidget); didUpdateRestorationId(); } /// Whether [restoreState] will be called at the beginning of the next build /// phase. /// /// Returns true when new restoration data has been provided to the mixin, but /// the registered [RestorableProperty]s have not been restored to their new /// values (as described by the new restoration data) yet. The properties will /// get the values restored when [restoreState] is invoked at the beginning of /// the next build cycle. /// /// While this is true, [bucket] will also still return the old bucket with /// the old restoration data. It will update to the new bucket with the new /// data just before [restoreState] is invoked. bool get restorePending { if (_firstRestorePending) { return true; } if (restorationId == null) { return false; } final RestorationBucket potentialNewParent = RestorationScope.of(context); return potentialNewParent != _currentParent && potentialNewParent?.isReplacing == true; } List<RestorableProperty<Object>> _debugPropertiesWaitingForReregistration; bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null; bool _firstRestorePending = true; RestorationBucket _currentParent; @override void didChangeDependencies() { super.didChangeDependencies(); final RestorationBucket oldBucket = _bucket; final bool needsRestore = restorePending; _currentParent = RestorationScope.of(context); final bool didReplaceBucket = _updateBucketIfNecessary(parent: _currentParent, restorePending: needsRestore); if (needsRestore) { _doRestore(oldBucket); } if (didReplaceBucket) { assert(oldBucket != _bucket); oldBucket?.dispose(); } } void _doRestore(RestorationBucket oldBucket) { assert(() { _debugPropertiesWaitingForReregistration = _properties.keys.toList(); return true; }()); restoreState(oldBucket, _firstRestorePending); _firstRestorePending = false; assert(() { if (_debugPropertiesWaitingForReregistration.isNotEmpty) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'Previously registered RestorableProperties must be re-registered in "restoreState".', ), ErrorDescription( 'The RestorableProperties with the following IDs were not re-registered to $this when ' '"restoreState" was called:', ), ..._debugPropertiesWaitingForReregistration.map((RestorableProperty<Object> property) => ErrorDescription( ' * ${property._restorationId}', )), ]); } _debugPropertiesWaitingForReregistration = null; return true; }()); } // Returns true if `bucket` has been replaced with a new bucket. It's the // responsibility of the caller to dispose the old bucket when this returns true. bool _updateBucketIfNecessary({ @required RestorationBucket parent, @required bool restorePending, }) { if (restorationId == null || parent == null) { final bool didReplace = _setNewBucketIfNecessary(newBucket: null, restorePending: restorePending); assert(_bucket == null); return didReplace; } assert(restorationId != null); assert(parent != null); if (restorePending || _bucket == null) { final RestorationBucket newBucket = parent.claimChild(restorationId, debugOwner: this); assert(newBucket != null); final bool didReplace = _setNewBucketIfNecessary(newBucket: newBucket, restorePending: restorePending); assert(_bucket == newBucket); return didReplace; } // We have an existing bucket, make sure it has the right parent and id. assert(_bucket != null); assert(!restorePending); _bucket.rename(restorationId); parent.adoptChild(_bucket); return false; } // Returns true if `bucket` has been replaced with a new bucket. It's the // responsibility of the caller to dispose the old bucket when this returns true. bool _setNewBucketIfNecessary({@required RestorationBucket newBucket, @required bool restorePending}) { if (newBucket == _bucket) { return false; } final RestorationBucket oldBucket = _bucket; _bucket = newBucket; if (!restorePending) { // Write the current property values into the new bucket to persist them. if (_bucket != null) { _properties.keys.forEach(_updateProperty); } didToggleBucket(oldBucket); } return true; } void _updateProperty(RestorableProperty<Object> property) { if (property.enabled) { _bucket?.write(property._restorationId, property.toPrimitives()); } else { _bucket?.remove<Object>(property._restorationId); } } void _unregister(RestorableProperty<Object> property) { final VoidCallback listener = _properties.remove(property); assert(() { _debugPropertiesWaitingForReregistration?.remove(property); return true; }()); property.removeListener(listener); property._unregister(); } @override void dispose() { _properties.forEach((RestorableProperty<Object> property, VoidCallback listener) { if (!property._disposed) { property.removeListener(listener); } }); _bucket?.dispose(); _bucket = null; super.dispose(); } }