blob: 42e33eec12ad364c539db3a68b44bdcfd911a9f1 [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 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'message_codec.dart';
import 'message_codecs.dart';
import 'system_channels.dart';
typedef _BucketVisitor = void Function(RestorationBucket bucket);
/// Manages the restoration data in the framework and synchronizes it with the
/// engine.
///
/// Restoration data can be serialized out and - at a later point in time - be
/// used to restore the application to the previous state described by the
/// serialized data. Mobile operating systems use the concept of state
/// restoration to provide the illusion that apps continue to run in the
/// background forever: after an app has been backgrounded, the user can always
/// return to it and find it in the same state. In practice, the operating
/// system may, however, terminate the app to free resources for other apps
/// running in the foreground. Before that happens, the app gets a chance to
/// serialize out its restoration data. When the user navigates back to the
/// backgrounded app, it is restarted and the serialized restoration data is
/// provided to it again. Ideally, the app will use that data to restore itself
/// to the same state it was in when the user backgrounded the app.
///
/// In Flutter, restoration data is organized in a tree of [RestorationBucket]s
/// which is rooted in the [rootBucket]. All information that the application
/// needs to restore its current state must be stored in a bucket in this
/// hierarchy. To store data in the hierarchy, entities (e.g. [Widget]s) must
/// claim ownership of a child bucket from a parent bucket (which may be the
/// [rootBucket] provided by this [RestorationManager]). The owner of a bucket
/// may store arbitrary values in the bucket as long as they can be serialized
/// with the [StandardMessageCodec]. The values are stored in the bucket under a
/// given restoration ID as key. A restoration ID is a [String] that must be
/// unique within a given bucket. To access the stored value again during state
/// restoration, the same restoration ID must be provided again. The owner of
/// the bucket may also make the bucket available to other entities so that they
/// can claim child buckets from it for their own restoration needs. Within a
/// bucket, child buckets are also identified by unique restoration IDs. The
/// restoration ID must be provided when claiming a child bucket.
///
/// When restoration data is provided to the [RestorationManager] (e.g. after
/// the application relaunched when foregrounded again), the bucket hierarchy
/// with all the data stored in it is restored. Entities can retrieve the data
/// again by using the same restoration IDs that they originally used to store
/// the data.
///
/// In addition to providing restoration data when the app is launched,
/// restoration data may also be provided to a running app to restore it to a
/// previous state (e.g. when the user hits the back/forward button in the web
/// browser). When this happens, the [RestorationManager] notifies its listeners
/// (added via [addListener]) that a new [rootBucket] is available. In response
/// to the notification, listeners must stop using the old bucket and restore
/// their state from the information in the new [rootBucket].
///
/// Same platforms restrict the size of the restoration data. Therefore, the
/// data stored in the buckets should be as small as possible while still
/// allowing the app to restore its current state from it. Data that can be
/// retrieved from other services (e.g. a database or a web server) should not
/// be included in the restoration data. Instead, a small identifier (e.g. a
/// UUID, database record number, or resource locator) should be stored that can
/// be used to retrieve the data again from its original source during state
/// restoration.
///
/// The [RestorationManager] sends a serialized version of the bucket hierarchy
/// over to the engine at the end of a frame in which the data in the hierarchy
/// or its shape has changed. The engine caches the data until the operating
/// system needs it. The application is responsible for keeping the data in the
/// bucket always up-to-date to reflect its current state.
///
/// ## Discussion
///
/// Due to Flutter's threading model and restrictions in the APIs of the
/// platforms Flutter runs on, restoration data must be stored in the buckets
/// proactively as described above. When the operating system asks for the
/// restoration data, it will do so on the platform thread expecting a
/// synchronous response. To avoid the risk of deadlocks, the platform thread
/// cannot block and call into the UI thread (where the dart code is running) to
/// retrieve the restoration data. For this reason, the [RestorationManager]
/// always sends the latest copy of the restoration data from the UI thread over
/// to the platform thread whenever it changes. That way, the restoration data
/// is always ready to go on the platform thread when the operating system needs
/// it.
///
/// See also:
///
/// * [ServicesBinding.restorationManager], which holds the singleton instance
/// of the [RestorationManager] for the currently running application.
/// * [RestorationBucket], which make up the restoration data hierarchy.
/// * [RestorationMixin], which uses [RestorationBucket]s behind the scenes
/// to make [State] objects of [StatefulWidget]s restorable.
class RestorationManager extends ChangeNotifier {
/// Construct the restoration manager and set up the communications channels
/// with the engine to get restoration messages (by calling [initChannels]).
RestorationManager() {
initChannels();
}
/// Sets up the method call handler for [SystemChannels.restoration].
///
/// This is called by the constructor to configure the communications channel
/// with the Flutter engine to get restoration messages.
///
/// Subclasses (especially in tests) can override this to avoid setting up
/// that communications channel, or to set it up differently, as necessary.
@protected
void initChannels() {
assert(!SystemChannels.restoration.checkMethodCallHandler(_methodHandler));
SystemChannels.restoration.setMethodCallHandler(_methodHandler);
}
/// The root of the [RestorationBucket] hierarchy containing the restoration
/// data.
///
/// Child buckets can be claimed from this bucket via
/// [RestorationBucket.claimChild]. If the [RestorationManager] has been asked
/// to restore the application to a previous state, these buckets will contain
/// the previously stored data. Otherwise the root bucket (and all children
/// claimed from it) will be empty.
///
/// The [RestorationManager] informs its listeners (added via [addListener])
/// when the value returned by this getter changes. This happens when new
/// restoration data has been provided to the [RestorationManager] to restore
/// the application to a different state. In response to the notification,
/// listeners must stop using the old root bucket and obtain the new one via
/// this getter ([rootBucket] will have been updated to return the new bucket
/// just before the listeners are notified).
///
/// The restoration data describing the current bucket hierarchy is retrieved
/// asynchronously from the engine the first time the root bucket is accessed
/// via this getter. After the data has been copied over from the engine, this
/// getter will return a [SynchronousFuture], that immediately resolves to the
/// root [RestorationBucket].
///
/// The returned [Future] may resolve to null if state restoration is
/// currently turned off.
///
/// See also:
///
/// * [RootRestorationScope], which makes the root bucket available in the
/// [Widget] tree.
Future<RestorationBucket?> get rootBucket {
if (_rootBucketIsValid) {
return SynchronousFuture<RestorationBucket?>(_rootBucket);
}
if (_pendingRootBucket == null) {
_pendingRootBucket = Completer<RestorationBucket>();
_getRootBucketFromEngine();
}
return _pendingRootBucket!.future;
}
RestorationBucket? _rootBucket; // May be null to indicate that restoration is turned off.
Completer<RestorationBucket>? _pendingRootBucket;
bool _rootBucketIsValid = false;
/// Returns true for the frame after [rootBucket] has been replaced with a
/// new non-null bucket.
///
/// When true, entities should forget their current state and restore
/// their state according to the information in the new [rootBucket].
///
/// The [RestorationManager] informs its listeners (added via [addListener])
/// when this flag changes from false to true.
bool get isReplacing => _isReplacing;
bool _isReplacing = false;
Future<void> _getRootBucketFromEngine() async {
final Map<dynamic, dynamic>? config = await SystemChannels.restoration.invokeMethod<Map<dynamic, dynamic>>('get');
if (_pendingRootBucket == null) {
// The restoration data was obtained via other means (e.g. by calling
// [handleRestorationDataUpdate] while the request to the engine was
// outstanding. Ignore the engine's response.
return;
}
assert(_rootBucket == null);
_parseAndHandleRestorationUpdateFromEngine(config);
}
void _parseAndHandleRestorationUpdateFromEngine(Map<dynamic, dynamic>? update) {
handleRestorationUpdateFromEngine(
enabled: update != null && update['enabled'] as bool,
data: update == null ? null : update['data'] as Uint8List,
);
}
/// Called by the [RestorationManager] on itself to parse the restoration
/// information obtained from the engine.
///
/// The `enabled` parameter indicates whether the engine wants to receive
/// restoration data. When `enabled` is false, state restoration is turned
/// off and the [rootBucket] is set to null. When `enabled` is true, the
/// provided restoration `data` will be parsed into a new [rootBucket]. If
/// `data` is null, an empty [rootBucket] will be instantiated.
///
/// Subclasses in test frameworks may call this method at any time to inject
/// restoration data (obtained e.g. by overriding [sendToEngine]) into the
/// [RestorationManager]. When the method is called before the [rootBucket] is
/// accessed, [rootBucket] will complete synchronously the next time it is
/// called.
@protected
void handleRestorationUpdateFromEngine({required bool enabled, required Uint8List? data}) {
assert(enabled != null);
assert(enabled || data == null);
_isReplacing = _rootBucketIsValid && enabled;
if (_isReplacing) {
SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
_isReplacing = false;
});
}
final RestorationBucket? oldRoot = _rootBucket;
_rootBucket = enabled
? RestorationBucket.root(manager: this, rawData: _decodeRestorationData(data))
: null;
_rootBucketIsValid = true;
assert(_pendingRootBucket == null || !_pendingRootBucket!.isCompleted);
_pendingRootBucket?.complete(_rootBucket);
_pendingRootBucket = null;
if (_rootBucket != oldRoot) {
notifyListeners();
oldRoot?.dispose();
}
}
/// Called by the [RestorationManager] on itself to send the provided
/// encoded restoration data to the engine.
///
/// The `encodedData` describes the entire bucket hierarchy that makes up the
/// current restoration data.
///
/// Subclasses in test frameworks may override this method to capture the
/// restoration data that would have been send to the engine. The captured
/// data can be re-injected into the [RestorationManager] via the
/// [handleRestorationUpdateFromEngine] method to restore the state described
/// by the data.
@protected
Future<void> sendToEngine(Uint8List encodedData) {
assert(encodedData != null);
return SystemChannels.restoration.invokeMethod<void>(
'put',
encodedData,
);
}
Future<dynamic> _methodHandler(MethodCall call) async {
switch (call.method) {
case 'push':
_parseAndHandleRestorationUpdateFromEngine(call.arguments as Map<dynamic, dynamic>);
break;
default:
throw UnimplementedError("${call.method} was invoked but isn't implemented by $runtimeType");
}
}
Map<dynamic, dynamic>? _decodeRestorationData(Uint8List? data) {
if (data == null) {
return null;
}
final ByteData encoded = data.buffer.asByteData(data.offsetInBytes, data.lengthInBytes);
return const StandardMessageCodec().decodeMessage(encoded) as Map<dynamic, dynamic>;
}
Uint8List _encodeRestorationData(Map<dynamic, dynamic> data) {
final ByteData encoded = const StandardMessageCodec().encodeMessage(data)!;
return encoded.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes);
}
bool _debugDoingUpdate = false;
bool _serializationScheduled = false;
final Set<RestorationBucket> _bucketsNeedingSerialization = <RestorationBucket>{};
/// Called by a [RestorationBucket] to request serialization for that bucket.
///
/// This method is called by a bucket in the hierarchy whenever the data
/// in it or the shape of the hierarchy has changed.
///
/// Calling this is a no-op when the bucket is already scheduled for
/// serialization.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@protected
@visibleForTesting
void scheduleSerializationFor(RestorationBucket bucket) {
assert(bucket != null);
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.add(bucket);
if (!_serializationScheduled) {
_serializationScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback((Duration _) => _doSerialization());
}
}
/// Called by a [RestorationBucket] to unschedule a request for serialization.
///
/// This method is called by a bucket in the hierarchy whenever it no longer
/// needs to be serialized (e.g. because the bucket got disposed).
///
/// It is safe to call this even when the bucket wasn't scheduled for
/// serialization before.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@protected
@visibleForTesting
void unscheduleSerializationFor(RestorationBucket bucket) {
assert(bucket != null);
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.remove(bucket);
}
void _doSerialization() {
if (!_serializationScheduled) {
return;
}
assert(() {
_debugDoingUpdate = true;
return true;
}());
_serializationScheduled = false;
for (final RestorationBucket bucket in _bucketsNeedingSerialization) {
bucket.finalize();
}
_bucketsNeedingSerialization.clear();
sendToEngine(_encodeRestorationData(_rootBucket!._rawData));
assert(() {
_debugDoingUpdate = false;
return true;
}());
}
/// Called to manually flush the restoration data to the engine.
///
/// A change in restoration data is usually accompanied by scheduling a frame
/// (because the restoration data is modified inside a [State.setState] call,
/// because it is usually something that affects the interface). Restoration
/// data is automatically flushed to the engine at the end of a frame. As a
/// result, it is uncommon to need to call this method directly. However, if
/// restoration data is changed without triggering a frame, this method must
/// be called to ensure that the updated restoration data is sent to the
/// engine in a timely manner. An example of such a use case is the
/// [Scrollable], where the final scroll offset after a scroll activity
/// finishes is determined between frames without scheduling a new frame.
///
/// Calling this method is a no-op if a frame is already scheduled. In that
/// case, the restoration data will be flushed to the engine at the end of
/// that frame. If this method is called and no frame is scheduled, the
/// current restoration data is directly sent to the engine.
void flushData() {
assert(!_debugDoingUpdate);
if (SchedulerBinding.instance!.hasScheduledFrame) {
return;
}
_doSerialization();
assert(!_serializationScheduled);
}
}
/// A [RestorationBucket] holds pieces of the restoration data that a part of
/// the application needs to restore its state.
///
/// For a general overview of how state restoration works in Flutter, see the
/// [RestorationManager].
///
/// [RestorationBucket]s are organized in a tree that is rooted in
/// [RestorationManager.rootBucket] and managed by a [RestorationManager]. The
/// tree is serializable and must contain all the data an application needs to
/// restore its current state at a later point in time.
///
/// A [RestorationBucket] stores restoration data as key-value pairs. The key is
/// a [String] representing a restoration ID that identifies a piece of data
/// uniquely within a bucket. The value can be anything that is serializable via
/// the [StandardMessageCodec]. Furthermore, a [RestorationBucket] may have
/// child buckets, which are identified within their parent via a unique
/// restoration ID as well.
///
/// During state restoration, the data previously stored in the
/// [RestorationBucket] hierarchy will be made available again to the
/// application to restore it to the state it had when the data was collected.
/// State restoration to a previous state may happen when the app is launched
/// (e.g. after it has been terminated gracefully while running in the
/// background) or after the app has already been running for a while.
///
/// ## Lifecycle
///
/// A [RestorationBucket] is rarely instantiated directly via its constructors.
/// Instead, when an entity wants to store data in or retrieve data from a
/// restoration bucket, it typically obtains a child bucket from a parent by
/// calling [claimChild]. If no parent is available,
/// [RestorationManager.rootBucket] may be used as a parent. When claiming a
/// child, the claimer must provide the restoration ID of the child it would
/// like to own. A child bucket with a given restoration ID can at most have
/// one owner. If another owner tries to claim a bucket with the same ID from
/// the same parent, an exception is thrown (see discussion in [claimChild]).
/// The restoration IDs that a given owner uses to claim a child (and to store
/// data in that child, see below) must be stable across app launches to ensure
/// that after the app restarts the owner can retrieve the same data again that
/// it stored during a previous run.
///
/// Per convention, the owner of the bucket has exclusive access to the values
/// stored in the bucket. It can read, add, modify, and remove values via the
/// [read], [write], and [remove] methods. In general, the owner should store
/// all the data in the bucket that it needs to restore its current state. If
/// its current state changes, the data in the bucket must be updated. At the
/// same time, the data in the bucket should be kept to a minimum. For example,
/// for data that can be retrieved from other sources (like a database or
/// webservice) only enough information (e.g. an ID or resource locator) to
/// re-obtain that data should be stored in the bucket. In addition to managing
/// the data in a bucket, an owner may also make the bucket available to other
/// entities so they can claim child buckets from it via [claimChild] for their
/// own restoration needs.
///
/// The bucket returned by [claimChild] may either contain state information
/// that the owner had previously (e.g. during a previous run of the
/// application) stored in it or it may be empty. If the bucket contains data,
/// the owner is expected to restore its state with the information previously
/// stored in the bucket. If the bucket is empty, it may initialize itself to
/// default values.
///
/// When the data stored in a bucket is no longer needed to restore the
/// application to its current state (e.g. because the owner of the bucket is no
/// longer shown on screen), the bucket must be [dispose]d. This will remove all
/// information stored in the bucket from the app's restoration data and that
/// information will not be available again when the application is restored to
/// this state in the future.
class RestorationBucket {
/// Creates an empty [RestorationBucket] to be provided to [adoptChild] to add
/// it to the bucket hierarchy.
///
/// {@template flutter.services.restoration.bucketcreation}
/// Instantiating a bucket directly is rare, most buckets are created by
/// claiming a child from a parent via [claimChild]. If no parent bucket is
/// available, [RestorationManager.rootBucket] may be used as a parent.
/// {@endtemplate}
///
/// The `restorationId` must not be null.
RestorationBucket.empty({
required String restorationId,
required Object? debugOwner,
}) : assert(restorationId != null),
_restorationId = restorationId,
_rawData = <String, dynamic>{} {
assert(() {
_debugOwner = debugOwner;
return true;
}());
}
/// Creates the root [RestorationBucket] for the provided restoration
/// `manager`.
///
/// The `rawData` must either be null (in which case an empty bucket will be
/// instantiated) or it must be a nested map describing the entire bucket
/// hierarchy in the following format:
///
/// ```javascript
/// {
/// 'v': { // key-value pairs
/// // * key is a string representation a restoration ID
/// // * value is any primitive that can be encoded with [StandardMessageCodec]
/// '<restoration-id>: <Object>,
/// },
/// 'c': { // child buckets
/// 'restoration-id': <nested map representing a child bucket>
/// }
/// }
/// ```
///
/// {@macro flutter.services.restoration.bucketcreation}
///
/// The `manager` argument must not be null.
RestorationBucket.root({
required RestorationManager manager,
required Map<dynamic, dynamic>? rawData,
}) : assert(manager != null),
_manager = manager,
_rawData = rawData ?? <dynamic, dynamic>{},
_restorationId = 'root' {
assert(() {
_debugOwner = manager;
return true;
}());
}
/// Creates a child bucket initialized with the data that the provided
/// `parent` has stored under the provided [restorationId].
///
/// This constructor cannot be used if the `parent` does not have any child
/// data stored under the given ID. In that case, create an empty bucket (via
/// [RestorationBucket.empty] and have the parent adopt it via [adoptChild].
///
/// {@macro flutter.services.restoration.bucketcreation}
///
/// The `restorationId` and `parent` argument must not be null.
RestorationBucket.child({
required String restorationId,
required RestorationBucket parent,
required Object? debugOwner,
}) : assert(restorationId != null),
assert(parent != null),
assert(parent._rawChildren[restorationId] != null),
_manager = parent._manager,
_parent = parent,
_rawData = parent._rawChildren[restorationId] as Map<dynamic, dynamic>,
_restorationId = restorationId {
assert(() {
_debugOwner = debugOwner;
return true;
}());
}
static const String _childrenMapKey = 'c';
static const String _valuesMapKey = 'v';
final Map<dynamic, dynamic> _rawData;
/// The owner of the bucket that was provided when the bucket was claimed via
/// [claimChild].
///
/// The value is used in error messages. Accessing the value is only valid
/// in debug mode, otherwise it will return null.
Object? get debugOwner {
assert(_debugAssertNotDisposed());
return _debugOwner;
}
Object? _debugOwner;
RestorationManager? _manager;
RestorationBucket? _parent;
/// Returns true when entities processing this bucket should restore their
/// state from the information in the bucket (e.g. via [read] and
/// [claimChild]) instead of copying their current state information into the
/// bucket (e.g. via [write] and [adoptChild].
///
/// This flag is true for the frame after the [RestorationManager] has been
/// instructed to restore the application from newly provided restoration
/// data.
bool get isReplacing => _manager?.isReplacing ?? false;
/// The restoration ID under which the bucket is currently stored in the
/// parent of this bucket (or wants to be stored if it is currently
/// parent-less).
///
/// This value is never null.
String get restorationId {
assert(_debugAssertNotDisposed());
return _restorationId;
}
String _restorationId;
// Maps a restoration ID to the raw map representation of a child bucket.
Map<dynamic, dynamic> get _rawChildren => _rawData.putIfAbsent(_childrenMapKey, () => <dynamic, dynamic>{}) as Map<dynamic, dynamic>;
// Maps a restoration ID to a value that is stored in this bucket.
Map<dynamic, dynamic> get _rawValues => _rawData.putIfAbsent(_valuesMapKey, () => <dynamic, dynamic>{}) as Map<dynamic, dynamic>;
// Get and store values.
/// Returns the value of type `P` that is currently stored in the bucket under
/// the provided `restorationId`.
///
/// Returns null if nothing is stored under that id. Throws, if the value
/// stored under the ID is not of type `P`.
///
/// See also:
///
/// * [write], which stores a value in the bucket.
/// * [remove], which removes a value from the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
P read<P>(String restorationId) {
assert(_debugAssertNotDisposed());
assert(restorationId != null);
return _rawValues[restorationId] as P;
}
/// Stores the provided `value` of type `P` under the provided `restorationId`
/// in the bucket.
///
/// Any value that has previously been stored under that ID is overwritten
/// with the new value. The provided `value` must be serializable with the
/// [StandardMessageCodec].
///
/// Null values will be stored in the bucket as-is. To remove a value, use
/// [remove].
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [remove], which removes a value from the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
void write<P>(String restorationId, P value) {
assert(_debugAssertNotDisposed());
assert(restorationId != null);
assert(debugIsSerializableForRestoration(value));
if (_rawValues[restorationId] != value || !_rawValues.containsKey(restorationId)) {
_rawValues[restorationId] = value;
_markNeedsSerialization();
}
}
/// Deletes the value currently stored under the provided `restorationId` from
/// the bucket.
///
/// The value removed from the bucket is casted to `P` and returned. If no
/// value was stored under that id, null is returned.
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [write], which stores a value in the bucket.
/// * [contains], which checks whether any value is stored under a given
/// restoration ID.
P remove<P>(String restorationId) {
assert(_debugAssertNotDisposed());
assert(restorationId != null);
final bool needsUpdate = _rawValues.containsKey(restorationId);
final P result = _rawValues.remove(restorationId) as P;
if (_rawValues.isEmpty) {
_rawData.remove(_valuesMapKey);
}
if (needsUpdate) {
_markNeedsSerialization();
}
return result;
}
/// Checks whether a value stored in the bucket under the provided
/// `restorationId`.
///
/// See also:
///
/// * [read], which retrieves a stored value from the bucket.
/// * [write], which stores a value in the bucket.
/// * [remove], which removes a value from the bucket.
bool contains(String restorationId) {
assert(_debugAssertNotDisposed());
assert(restorationId != null);
return _rawValues.containsKey(restorationId);
}
// Child management.
// The restoration IDs and associated buckets of children that have been
// claimed via [claimChild].
final Map<String, RestorationBucket> _claimedChildren = <String, RestorationBucket>{};
// Newly created child buckets whose restoration ID is still in use, see
// comment in [claimChild] for details.
final Map<String, List<RestorationBucket>> _childrenToAdd = <String, List<RestorationBucket>>{};
/// Claims ownership of the child with the provided `restorationId` from this
/// bucket.
///
/// If the application is getting restored to a previous state, the bucket
/// will contain all the data that was previously stored in the bucket.
/// Otherwise, an empty bucket is returned.
///
/// The claimer of the bucket is expected to use the data stored in the bucket
/// to restore itself to its previous state described by the data in the
/// bucket. If the bucket is empty, it should initialize itself to default
/// values. Whenever the information that the claimer needs to restore its
/// state changes, the data in the bucket should be updated to reflect that.
///
/// A child bucket with a given `restorationId` can only have one owner. If
/// another owner claims a child bucket with the same `restorationId` an
/// exception will be thrown at the end of the current frame unless the
/// previous owner has either deleted its bucket by calling [dispose] or has
/// moved it to a new parent via [adoptChild].
///
/// When the returned bucket is no longer needed, it must be [dispose]d to
/// delete the information stored in it from the app's restoration data.
RestorationBucket claimChild(String restorationId, {required Object? debugOwner}) {
assert(_debugAssertNotDisposed());
assert(restorationId != null);
// There are three cases to consider:
// 1. Claiming an ID that has already been claimed.
// 2. Claiming an ID that doesn't yet exist in [_rawChildren].
// 3. Claiming an ID that does exist in [_rawChildren] and hasn't been
// claimed yet.
// If an ID has already been claimed (case 1) the current owner may give up
// that ID later this frame and it can be re-used. In anticipation of the
// previous owner's surrender of the id, we return an empty bucket for this
// new claim and check in [_debugAssertIntegrity] that at the end of the
// frame the old owner actually did surrendered the id.
// Case 2 also requires the creation of a new empty bucket.
// In Case 3 we create a new bucket wrapping the existing data in
// [_rawChildren].
// Case 1+2: Adopt and return an empty bucket.
if (_claimedChildren.containsKey(restorationId) || !_rawChildren.containsKey(restorationId)) {
final RestorationBucket child = RestorationBucket.empty(
debugOwner: debugOwner,
restorationId: restorationId,
);
adoptChild(child);
return child;
}
// Case 3: Return bucket wrapping the existing data.
assert(_rawChildren[restorationId] != null);
final RestorationBucket child = RestorationBucket.child(
restorationId: restorationId,
parent: this,
debugOwner: debugOwner,
);
_claimedChildren[restorationId] = child;
return child;
}
/// Adopts the provided `child` bucket.
///
/// The `child` will be dropped from its old parent, if it had one.
///
/// The `child` is stored under its [restorationId] in this bucket. If this
/// bucket already contains a child bucket under the same id, the owner of
/// that existing bucket must give it up (e.g. by moving the child bucket to a
/// different parent or by disposing it) before the end of the current frame.
/// Otherwise an exception indicating the illegal use of duplicated
/// restoration IDs will trigger in debug mode.
///
/// No-op if the provided bucket is already a child of this bucket.
void adoptChild(RestorationBucket child) {
assert(_debugAssertNotDisposed());
assert(child != null);
if (child._parent != this) {
child._parent?._removeChildData(child);
child._parent = this;
_addChildData(child);
if (child._manager != _manager) {
_recursivelyUpdateManager(child);
}
}
assert(child._parent == this);
assert(child._manager == _manager);
}
void _dropChild(RestorationBucket child) {
assert(child != null);
assert(child._parent == this);
_removeChildData(child);
child._parent = null;
if (child._manager != null) {
child._updateManager(null);
child._visitChildren(_recursivelyUpdateManager);
}
}
bool _needsSerialization = false;
void _markNeedsSerialization() {
if (!_needsSerialization) {
_needsSerialization = true;
_manager?.scheduleSerializationFor(this);
}
}
/// Called by the [RestorationManager] just before the data of the bucket
/// is serialized and send to the engine.
///
/// It is exposed to allow testing of [RestorationBucket]s in isolation.
@visibleForTesting
void finalize() {
assert(_debugAssertNotDisposed());
assert(_needsSerialization);
_needsSerialization = false;
assert(_debugAssertIntegrity());
}
void _recursivelyUpdateManager(RestorationBucket bucket) {
bucket._updateManager(_manager);
bucket._visitChildren(_recursivelyUpdateManager);
}
void _updateManager(RestorationManager? newManager) {
if (_manager == newManager) {
return;
}
if (_needsSerialization) {
_manager?.unscheduleSerializationFor(this);
}
_manager = newManager;
if (_needsSerialization && _manager != null) {
_needsSerialization = false;
_markNeedsSerialization();
}
}
bool _debugAssertIntegrity() {
assert(() {
if (_childrenToAdd.isEmpty) {
return true;
}
final List<DiagnosticsNode> error = <DiagnosticsNode>[
ErrorSummary('Multiple owners claimed child RestorationBuckets with the same IDs.'),
ErrorDescription('The following IDs were claimed multiple times from the parent $this:')
];
for (final MapEntry<String, List<RestorationBucket>> child in _childrenToAdd.entries) {
final String id = child.key;
final List<RestorationBucket> buckets = child.value;
assert(buckets.isNotEmpty);
assert(_claimedChildren.containsKey(id));
error.addAll(<DiagnosticsNode>[
ErrorDescription(' * "$id" was claimed by:'),
...buckets.map((RestorationBucket bucket) => ErrorDescription(' * ${bucket.debugOwner}')),
ErrorDescription(' * ${_claimedChildren[id]!.debugOwner} (current owner)'),
]);
}
throw FlutterError.fromParts(error);
}());
return true;
}
void _removeChildData(RestorationBucket child) {
assert(child != null);
assert(child._parent == this);
if (_claimedChildren.remove(child.restorationId) == child) {
_rawChildren.remove(child.restorationId);
final List<RestorationBucket>? pendingChildren = _childrenToAdd[child.restorationId];
if (pendingChildren != null) {
final RestorationBucket toAdd = pendingChildren.removeLast();
_finalizeAddChildData(toAdd);
if (pendingChildren.isEmpty) {
_childrenToAdd.remove(child.restorationId);
}
}
if (_rawChildren.isEmpty) {
_rawData.remove(_childrenMapKey);
}
_markNeedsSerialization();
return;
}
_childrenToAdd[child.restorationId]?.remove(child);
if (_childrenToAdd[child.restorationId]?.isEmpty == true) {
_childrenToAdd.remove(child.restorationId);
}
}
void _addChildData(RestorationBucket child) {
assert(child != null);
assert(child._parent == this);
if (_claimedChildren.containsKey(child.restorationId)) {
// Delay addition until the end of the frame in the hopes that the current
// owner of the child with the same ID will have given up that child by
// then.
_childrenToAdd.putIfAbsent(child.restorationId, () => <RestorationBucket>[]).add(child);
_markNeedsSerialization();
return;
}
_finalizeAddChildData(child);
_markNeedsSerialization();
}
void _finalizeAddChildData(RestorationBucket child) {
assert(_claimedChildren[child.restorationId] == null);
assert(_rawChildren[child.restorationId] == null);
_claimedChildren[child.restorationId] = child;
_rawChildren[child.restorationId] = child._rawData;
}
void _visitChildren(_BucketVisitor visitor, {bool concurrentModification = false}) {
Iterable<RestorationBucket> children = _claimedChildren.values
.followedBy(_childrenToAdd.values.expand((List<RestorationBucket> buckets) => buckets));
if (concurrentModification) {
children = children.toList(growable: false);
}
children.forEach(visitor);
}
// Bucket management
/// Changes the restoration ID under which the bucket is (or will be) stored
/// in its parent to `newRestorationId`.
///
/// No-op if the bucket is already stored under the provided id.
///
/// If another owner has already claimed a bucket with the provided `newId` an
/// exception will be thrown at the end of the current frame unless the other
/// owner has deleted its bucket by calling [dispose], [rename]ed it using
/// another ID, or has moved it to a new parent via [adoptChild].
void rename(String newRestorationId) {
assert(_debugAssertNotDisposed());
assert(newRestorationId != null);
if (newRestorationId == restorationId) {
return;
}
_parent?._removeChildData(this);
_restorationId = newRestorationId;
_parent?._addChildData(this);
}
/// Deletes the bucket and all the data stored in it from the bucket
/// hierarchy.
///
/// After [dispose] has been called, the data stored in this bucket and its
/// children are no longer part of the app's restoration data. The data
/// originally stored in the bucket will not be available again when the
/// application is restored to this state in the future. It is up to the
/// owners of the children to either move them (via [adoptChild]) to a new
/// parent that is still part of the bucket hierarchy or to [dispose] of them
/// as well.
///
/// This method must only be called by the object's owner.
void dispose() {
assert(_debugAssertNotDisposed());
_visitChildren(_dropChild, concurrentModification: true);
_claimedChildren.clear();
_childrenToAdd.clear();
_parent?._removeChildData(this);
_parent = null;
_updateManager(null);
_debugDisposed = true;
}
@override
String toString() => '${objectRuntimeType(this, 'RestorationBucket')}(restorationId: $restorationId, owner: $debugOwner)';
bool _debugDisposed = false;
bool _debugAssertNotDisposed() {
assert(() {
if (_debugDisposed) {
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;
}
}
/// Returns true when the provided `object` is serializable for state
/// restoration.
///
/// Should only be called from within asserts. Always returns false outside
/// of debug builds.
bool debugIsSerializableForRestoration(Object? object) {
bool result = false;
assert(() {
try {
const StandardMessageCodec().encodeMessage(object);
result = true;
} catch (_) {
result = false;
}
return true;
}());
return result;
}