blob: 24a195ebe2875ead77c7e12ad3900b95a3987ca4 [file] [log] [blame]
// 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.
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.RestorationScope}
/// 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({
super.key,
required this.restorationId,
required this.child,
}) : assert(child != null);
/// 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.ProxyWidget.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.RestorationScope}
///
/// 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({
super.key,
this.bucket,
required super.child,
}) : assert(child != null);
/// 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({
super.key,
required this.restorationId,
required this.child,
}) : assert(child != null);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.ProxyWidget.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() {
if (_rootBucketValid) {
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(ChangeNotifier.debugAssertNotDisposed(this)); // FYI, This uses ChangeNotifier's _debugDisposed, not _disposed.
_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(ChangeNotifier.debugAssertNotDisposed(this));
assert(restorationId != null);
assert(owner != null);
_restorationId = restorationId;
_owner = owner;
}
void _unregister() {
assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(_restorationId != null);
assert(_owner != null);
_restorationId = null;
_owner = null;
}
/// The [State] object that this property is registered with.
///
/// Must only be called when [isRegistered] is true.
@protected
State get state {
assert(isRegistered);
assert(ChangeNotifier.debugAssertNotDisposed(this));
return _owner!;
}
/// Whether this property is currently registered with a [RestorationMixin].
@protected
bool get isRegistered {
assert(ChangeNotifier.debugAssertNotDisposed(this));
return _restorationId != null;
}
}
/// 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}
/// This example demonstrates how to make a simple counter app restorable by
/// using the [RestorationMixin] and a [RestorableInt].
///
/// ** See code in examples/api/lib/widgets/restoration/restoration_mixin.0.dart **
/// {@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 mode).
///
/// 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) ?? false;
final Object? initialValue = hasSerializedValue
? property.fromPrimitives(bucket!.read<Object>(restorationId))
: property.createDefaultValue();
if (!property.isRegistered) {
property._register(restorationId, this);
void 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 didChangeDependencies
// 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 ?? false);
}
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();
}
}