Unverified Commit 175e5c9a authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Restoration Framework (#60375)

* state restoration

* added example

* typos and analyzer

* whitespace

* more typos

* remove unnecessary import

* whitespace

* fix sample code

* tests for restorationmanager and restorationid

* ++

* typo

* tests for bucket, part1

* rename tests

* more tests

* finished tests for service layer

* remove wrong todo

* ++

* review comments

* tests for Unmanaged and regular scope

* RootRestorationScope tests

* typo

* whitespace

* testing framework

* tests for properties

* last set of tests

* analyzer

* typo

* dan review

* whitespace

* ++

* refactor finalizers

* ++

* ++

* dispose guard

* ++

* ++

* dan review

* add manager assert

* ++

* analyzer

* greg review

* fix typo

* Ian & John review

* ian review

* RestorationID -> String

* revert comment

* Make primitives non-nullable in prep for NNBD
parent 1fff1050
...@@ -33,6 +33,7 @@ export 'src/services/raw_keyboard_linux.dart'; ...@@ -33,6 +33,7 @@ export 'src/services/raw_keyboard_linux.dart';
export 'src/services/raw_keyboard_macos.dart'; export 'src/services/raw_keyboard_macos.dart';
export 'src/services/raw_keyboard_web.dart'; export 'src/services/raw_keyboard_web.dart';
export 'src/services/raw_keyboard_windows.dart'; export 'src/services/raw_keyboard_windows.dart';
export 'src/services/restoration.dart';
export 'src/services/system_channels.dart'; export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart'; export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart'; export 'src/services/system_navigator.dart';
......
...@@ -13,6 +13,7 @@ import 'package:flutter/scheduler.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter/scheduler.dart';
import 'asset_bundle.dart'; import 'asset_bundle.dart';
import 'binary_messenger.dart'; import 'binary_messenger.dart';
import 'restoration.dart';
import 'system_channels.dart'; import 'system_channels.dart';
/// Listens for platform messages and directs them to the [defaultBinaryMessenger]. /// Listens for platform messages and directs them to the [defaultBinaryMessenger].
...@@ -27,6 +28,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { ...@@ -27,6 +28,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
super.initInstances(); super.initInstances();
_instance = this; _instance = this;
_defaultBinaryMessenger = createBinaryMessenger(); _defaultBinaryMessenger = createBinaryMessenger();
_restorationManager = createRestorationManager();
window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage; window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
initLicenses(); initLicenses();
SystemChannels.system.setMessageHandler(handleSystemMessage); SystemChannels.system.setMessageHandler(handleSystemMessage);
...@@ -204,6 +206,27 @@ mixin ServicesBinding on BindingBase, SchedulerBinding { ...@@ -204,6 +206,27 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
} }
return null; return null;
} }
/// The [RestorationManager] synchronizes the restoration data between
/// engine and framework.
///
/// See the docs for [RestorationManager] for a discussion of restoration
/// state and how it is organized in Flutter.
///
/// To use a different [RestorationManager] subclasses can override
/// [createRestorationManager], which is called to create the instance
/// returned by this getter.
RestorationManager get restorationManager => _restorationManager;
RestorationManager _restorationManager;
/// Creates the [RestorationManager] instance available via
/// [restorationManager].
///
/// Can be overriden in subclasses to create a different [RestorationManager].
@protected
RestorationManager createRestorationManager() {
return RestorationManager();
}
} }
/// The default implementation of [BinaryMessenger]. /// The default implementation of [BinaryMessenger].
......
// 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 '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 [Sting] 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 current bucket hierarchy is decommissioned
/// and replaced with the hierarchy deserialized from the newly provided
/// restoration data. Buckets in the old hierarchy notify their listeners when
/// they get decommissioned. In response to the notification, listeners must
/// stop using the old buckets. Owners of those buckets must dispose of them and
/// claim a new child as a replacement from a parent in the new bucket hierarchy
/// (that parent may be the updated [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 {
/// 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 (!SystemChannels.restoration.checkMethodCallHandler(_methodHandler)) {
SystemChannels.restoration.setMethodCallHandler(_methodHandler);
}
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;
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 the [rootBucket]. If
/// `data` is null, an empty [rootBucket] will be instantiated.
///
/// When this method is called, the old [rootBucket] is decommissioned.
///
/// 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);
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();
}
if (oldRoot != null) {
oldRoot
..decommission()
..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) {
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");
}
return null;
}
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 _postFrameScheduled = 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 (!_postFrameScheduled) {
_postFrameScheduled = 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() {
assert(() {
_debugDoingUpdate = true;
return true;
}());
_postFrameScheduled = false;
for (final RestorationBucket bucket in _bucketsNeedingSerialization) {
bucket.finalize();
}
_bucketsNeedingSerialization.clear();
sendToEngine(_encodeRestorationData(_rootBucket._rawData));
assert(() {
_debugDoingUpdate = false;
return true;
}());
}
}
/// 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.
///
/// During the lifetime of a bucket, it may notify its listeners that the bucket
/// has been [decommission]ed. This happens when new restoration data has been
/// provided to, for example, the [RestorationManager] to restore the
/// application to a different state (e.g. when the user hits the back/forward
/// button in the web browser). In response to the notification, owners must
/// dispose their current bucket and replace it with a new bucket claimed from a
/// new parent (which will have been initialized with the new restoration data).
/// For example, if the owner previously claimed its bucket from
/// [RestorationManager.rootBucket], it must claim its new bucket from there
/// again. The root bucket will have been replaced with the new root bucket just
/// before the bucket listeners are informed about the decommission. Once the
/// new bucket is obtained, owners should restore their internal state according
/// to the information in the new bucket.
///
/// 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 extends ChangeNotifier {
/// 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 [id].
///
/// 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;
/// 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>;
/// Called to signal that this bucket and all its descendants are no longer
/// part of the current restoration data and must not be used anymore.
///
/// Calling this method will drop this bucket from its parent and notify all
/// its listeners as well as all listeners of its descendants. Once a bucket
/// has notified its listeners, it must not be used anymore. During the next
/// frame following the notification, the bucket must be disposed and replaced
/// with a new bucket.
///
/// As an example, the [RestorationManager] calls this method on its root
/// bucket when it has been asked to restore a running application to a
/// different state. At that point, the data stored in the current bucket
/// hierarchy is invalid and will be replaced with a new hierarchy generated
/// from the restoration data describing the new state. To replace the current
/// bucket hierarchy, [decommission] is called on the root bucket to signal to
/// all owners of buckets in the hierarchy that their bucket has become
/// invalid. In response to the notification, bucket owners must [dispose]
/// their buckets and claim a new bucket from the newly created hierarchy. For
/// example, the owner of a bucket that was originally claimed from the
/// [RestorationManager.rootBucket] must dispose that bucket and claim a new
/// bucket from the new [RestorationManager.rootBucket]. Once the new bucket
/// is claimed, owners should restore their state according to the data stored
/// in the new bucket.
void decommission() {
assert(_debugAssertNotDisposed());
if (_parent != null) {
_parent._dropChild(this);
_parent = null;
}
_performDecommission();
}
bool _decommissioned = false;
void _performDecommission() {
_decommissioned = true;
_updateManager(null);
notifyListeners();
_visitChildren((RestorationBucket bucket) {
bucket._performDecommission();
});
}
// 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 [id] 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() {
assert(_manager != null || _decommissioned);
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 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);
assert(_parent != 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.
@override
void dispose() {
assert(_debugAssertNotDisposed());
_visitChildren(_dropChild, concurrentModification: true);
_claimedChildren.clear();
_childrenToAdd.clear();
_parent?._removeChildData(this);
_parent = null;
_updateManager(null);
super.dispose();
_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;
}
...@@ -286,4 +286,35 @@ class SystemChannels { ...@@ -286,4 +286,35 @@ class SystemChannels {
'flutter/mousecursor', 'flutter/mousecursor',
StandardMethodCodec(), StandardMethodCodec(),
); );
/// A [MethodChannel] for synchronizing restoration data with the engine.
///
/// The following outgoing methods are defined for this channel (invoked using
/// [OptionalMethodChannel.invokeMethod]):
///
/// * `get`: Retrieves the current restoration information (e.g. provided by
/// the operating system) from the engine. The method returns a map
/// containing an `enabled` boolean to indicate whether collecting
/// restoration data is supported by the embedder. If `enabled` is true,
/// the map may also contain restoration data stored under the `data` key
/// from which the state of the framework may be restored. The restoration
/// data is encoded as [Uint8List].
/// * `put`: Sends the current restoration data to the engine. Takes the
/// restoration data encoded as [Uint8List] as argument.
///
/// The following incoming methods are defined for this channel (registered
/// using [MethodChannel.setMethodCallHandler]).
///
/// * `push`: Called by the engine to send newly provided restoration
/// information to the framework. The argument given to this method has
/// the same format as the object that the `get` method returns.
///
/// See also:
///
/// * [RestorationManager], which uses this channel and also describes how
/// restoration data is used in Flutter.
static const MethodChannel restoration = OptionalMethodChannel(
'flutter/restoration',
StandardMethodCodec(),
);
} }
// 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) {
// 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) {
/// // TODO(goderbauer): remove the [RootRestorationScope] once it is part of [MaterialApp]. // ignore: flutter_style_todos
/// 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) {
/// // 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
/// 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);
/// 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 restore is pending, restoreState must be called instead.
assert(!restorePending);
}
// 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() {
if (_bucket?.restorationId != restorationId && !restorePending) {
_updateBucketIfNecessary();
}
}
@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 => _restorePending;
bool _restorePending = true;
List<RestorableProperty<Object>> _debugPropertiesWaitingForReregistration;
bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null;
@override
void didChangeDependencies() {
super.didChangeDependencies();
RestorationBucket oldBucket;
if (_restorePending) {
oldBucket = _bucket;
// Throw away the old bucket so [_updateBucketIfNecessary] will claim a
// new one with the new restoration data.
_bucket = null;
}
_updateBucketIfNecessary();
if (_restorePending) {
_restorePending = false;
assert(() {
_debugPropertiesWaitingForReregistration = _properties.keys.toList();
return true;
}());
restoreState(oldBucket);
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;
}());
oldBucket?.dispose();
}
}
void _markNeedsRestore() {
_restorePending = true;
// [didChangeDependencies] will be called next because our bucket can only
// become invalid if our parent bucket ([RestorationScope.of]) is replaced
// with a new one.
}
void _updateBucketIfNecessary() {
if (restorationId == null) {
_setNewBucketIfNecessary(newBucket: null);
assert(_bucket == null);
return;
}
final RestorationBucket newParent = RestorationScope.of(context);
if (newParent == null) {
_setNewBucketIfNecessary(newBucket: null);
assert(_bucket == null);
return;
}
if (_bucket == null) {
assert(newParent != null);
assert(restorationId != null);
final RestorationBucket newBucket = newParent.claimChild(restorationId, debugOwner: this)
..addListener(_markNeedsRestore);
assert(newBucket != null);
_setNewBucketIfNecessary(newBucket: newBucket);
assert(_bucket == newBucket);
return;
}
// We have an existing bucket, make sure it has the right parent and id.
assert(_bucket != null);
assert(newParent != null);
assert(restorationId != null);
_bucket.rename(restorationId);
newParent.adoptChild(_bucket);
}
void _setNewBucketIfNecessary({@required RestorationBucket newBucket}) {
if (newBucket == _bucket) {
return;
}
assert(newBucket == null || _bucket == null);
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);
}
oldBucket?.dispose();
}
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();
}
}
// 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/services.dart';
import 'editable_text.dart';
import 'restoration.dart';
/// A [RestorableProperty] that makes the wrapped value accessible to the owning
/// [State] object via the [value] getter and setter.
///
/// Whenever a new [value] is set, [didUpdateValue] is called. Subclasses should
/// call [notifyListeners] from this method if the new value changes what
/// [toPrimitives] returns.
///
/// See also:
///
/// * [RestorableProperty], which is the super class of this class.
/// * [RestorationMixin], to which a [RestorableValue] needs to be registered
/// in order to work.
/// * [RestorationManager], which provides an overview of how state restoration
/// works in Flutter.
abstract class RestorableValue<T> extends RestorableProperty<T> {
/// The current value stored in this property.
///
/// A representation of the current value is stored in the restoration data.
/// During state restoration, the property will restore the value to what it
/// was when the restoration data it is getting restored from was collected.
///
/// The [value] can only be accessed after the property has been registered
/// with a [RestorationMixin] by calling
/// [RestorationMixin.registerForRestoration].
T get value {
assert(isRegistered);
return _value;
}
T _value;
set value(T newValue) {
assert(isRegistered);
if (newValue != _value) {
final T oldValue = _value;
_value = newValue;
didUpdateValue(oldValue);
}
}
@mustCallSuper
@override
void initWithValue(T value) {
_value = value;
}
/// Called whenever a new value is assigned to [value].
///
/// The new value can be accessed via the regular [value] getter and the
/// previous value is provided as `oldValue`.
///
/// Subclasses should call [notifyListeners] from this method, if the new
/// value changes what [toPrimitives] returns.
@protected
void didUpdateValue(T oldValue);
}
// _RestorablePrimitiveValue and its subclasses do not allow null values in
// anticipation of NNBD (non-nullability by default).
//
// If necessary, we can in the future define a new subclass hierarchy that
// does allow null values for primitive types. Borrowing from lisp where
// functions that returned a bool ended in 'p', a suggested naming scheme for
// these new subclasses could be to add 'N' (for nullable) to the end of a
// class name (e.g. RestorableIntN, RestorableStringN, etc.) to distinguish them
// from their non-nullable friends.
class _RestorablePrimitiveValue<T> extends RestorableValue<T> {
_RestorablePrimitiveValue(this._defaultValue)
: assert(_defaultValue != null),
assert(debugIsSerializableForRestoration(_defaultValue)),
super();
final T _defaultValue;
@override
T createDefaultValue() => _defaultValue;
@override
set value(T value) {
assert(value != null);
super.value = value;
}
@override
void didUpdateValue(T oldValue) {
assert(debugIsSerializableForRestoration(value));
notifyListeners();
}
@override
T fromPrimitives(Object serialized) {
assert(serialized != null);
return serialized as T;
}
@override
Object toPrimitives() {
assert(value != null);
return value;
}
}
/// A [RestorableProperty] that knows how to store and restore a [num].
///
/// {@template flutter.widgets.restoration.primitivevalue}
/// The current [value] of this property is stored in the restoration data.
/// During state restoration the property is restored to the value it had when
/// the restoration data it is getting restored from was collected.
///
/// If no restoration data is available, [value] is initialized to the
/// `defaultValue` given in the constructor.
/// {@endtemplate}
///
/// Instead of using the more generic [RestorableNum] directly, consider using
/// one of the more specific subclasses (e.g. [RestorableDouble] to store a
/// [double] and [RestorableInt] to store an [int]).
class RestorableNum<T extends num> extends _RestorablePrimitiveValue<T> {
/// Creates a [RestorableNum].
///
/// {@template flutter.widgets.restoration.primitivevalue.constructor}
/// If no restoration data is available to restore the value in this property
/// from, the property will be initialized with the provided `defaultValue`.
/// {@endtemplate}
RestorableNum(T defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [double].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableDouble extends RestorableNum<double> {
/// Creates a [RestorableDouble].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableDouble(double defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore an [int].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableInt extends RestorableNum<int> {
/// Creates a [RestorableInt].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableInt(int defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [String].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableString extends _RestorablePrimitiveValue<String> {
/// Creates a [RestorableString].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableString(String defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [bool].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableBool extends _RestorablePrimitiveValue<bool> {
/// Creates a [RestorableBool].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableBool(bool defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A base class for creating a [RestorableProperty] that stores and restores a
/// [Listenable].
///
/// This class may be used to implement a [RestorableProperty] for a
/// [Listenable], whose information it needs to store in the restoration data
/// change whenever the [Listenable] notifies its listeners.
///
/// The [RestorationMixin] this property is registered with will call
/// [toPrimitives] whenever the wrapped [Listenable] notifies its listeners to
/// update the information that this property has stored in the restoration
/// data.
abstract class RestorableListenable<T extends Listenable> extends RestorableProperty<T> {
/// The [Listenable] stored in this property.
///
/// A representation of the current value of the [Listenable] is stored in the
/// restoration data. During state restoration, the [Listenable] returned by
/// this getter will be restored to the state it had when the restoration data
/// the property is getting restored from was collected.
///
/// The [value] can only be accessed after the property has been registered
/// with a [RestorationMixin] by calling
/// [RestorationMixin.registerForRestoration].
T get value {
assert(isRegistered);
return _value;
}
T _value;
@override
void initWithValue(T value) {
assert(value != null);
_value?.removeListener(notifyListeners);
_value = value;
_value.addListener(notifyListeners);
}
@override
void dispose() {
super.dispose();
_value?.removeListener(notifyListeners);
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [TextEditingController].
///
/// The [TextEditingController] is accessible via the [value] getter. During
/// state restoration, the property will restore [TextEditingController.text] to
/// the value it had when the restoration data it is getting restored from was
/// collected.
class RestorableTextEditingController extends RestorableListenable<TextEditingController> {
/// Creates a [RestorableTextEditingController].
///
/// This constructor treats a null `text` argument as if it were the empty
/// string.
factory RestorableTextEditingController({String text}) => RestorableTextEditingController.fromValue(
text == null ? TextEditingValue.empty : TextEditingValue(text: text),
);
/// Creates a [RestorableTextEditingController] from an initial
/// [TextEditingValue].
///
/// This constructor treats a null `value` argument as if it were
/// [TextEditingValue.empty].
RestorableTextEditingController.fromValue(TextEditingValue value) : _initialValue = value;
final TextEditingValue _initialValue;
@override
TextEditingController createDefaultValue() {
return TextEditingController.fromValue(_initialValue);
}
@override
TextEditingController fromPrimitives(Object data) {
return TextEditingController(text: data as String);
}
@override
Object toPrimitives() {
return value.text;
}
@override
void dispose() {
if (isRegistered) {
value.dispose();
}
super.dispose();
}
}
...@@ -82,6 +82,8 @@ export 'src/widgets/platform_view.dart'; ...@@ -82,6 +82,8 @@ export 'src/widgets/platform_view.dart';
export 'src/widgets/preferred_size.dart'; export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/restoration.dart';
export 'src/widgets/restoration_properties.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/safe_area.dart'; export 'src/widgets/safe_area.dart';
export 'src/widgets/scroll_activity.dart'; export 'src/widgets/scroll_activity.dart';
......
// 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 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
class MockRestorationManager extends TestRestorationManager {
bool get updateScheduled => _updateScheduled;
bool _updateScheduled = false;
final List<RestorationBucket> _buckets = <RestorationBucket>[];
@override
void scheduleSerializationFor(RestorationBucket bucket) {
_updateScheduled = true;
_buckets.add(bucket);
}
@override
bool unscheduleSerializationFor(RestorationBucket bucket) {
_updateScheduled = true;
return _buckets.remove(bucket);
}
void doSerialization() {
_updateScheduled = false;
for (final RestorationBucket bucket in _buckets) {
bucket.finalize();
}
_buckets.clear();
}
@override
void restoreFrom(TestRestorationData data) {
// Ignore in mock.
}
int rootBucketAccessed = 0;
@override
Future<RestorationBucket> get rootBucket {
rootBucketAccessed++;
return _rootBucket;
}
Future<RestorationBucket> _rootBucket;
set rootBucket(Future<RestorationBucket> value) {
_rootBucket = value;
notifyListeners();
}
@override
Future<void> sendToEngine(Uint8List encodedData) {
throw UnimplementedError('unimplemented in mock');
}
@override
String toString() => 'MockManager';
}
const String childrenMapKey = 'c';
const String valuesMapKey = 'v';
// 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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
test('root bucket values', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
expect(bucket.restorationId, 'root');
expect(bucket.debugOwner, manager);
// Bucket contains expected values from rawData.
expect(bucket.read<int>('value1'), 10);
expect(bucket.read<String>('value2'), 'Hello');
expect(bucket.read<String>('value3'), isNull); // Does not exist.
expect(manager.updateScheduled, isFalse);
// Can overwrite existing value.
bucket.write<int>('value1', 22);
expect(manager.updateScheduled, isTrue);
expect(bucket.read<int>('value1'), 22);
manager.doSerialization();
expect(rawData[valuesMapKey]['value1'], 22);
expect(manager.updateScheduled, isFalse);
// Can add a new value.
bucket.write<bool>('value3', true);
expect(manager.updateScheduled, isTrue);
expect(bucket.read<bool>('value3'), true);
manager.doSerialization();
expect(rawData[valuesMapKey]['value3'], true);
expect(manager.updateScheduled, isFalse);
// Can remove existing value.
expect(bucket.remove<int>('value1'), 22);
expect(manager.updateScheduled, isTrue);
expect(bucket.read<int>('value1'), isNull); // Does not exist anymore.
manager.doSerialization();
expect(rawData[valuesMapKey].containsKey('value1'), isFalse);
expect(manager.updateScheduled, isFalse);
// Removing non-existing value is no-op.
expect(bucket.remove<Object>('value4'), isNull);
expect(manager.updateScheduled, isFalse);
// Can store null.
bucket.write<bool>('value4', null);
expect(manager.updateScheduled, isTrue);
expect(bucket.read<int>('value4'), null);
manager.doSerialization();
expect(rawData[valuesMapKey].containsKey('value4'), isTrue);
expect(rawData[valuesMapKey]['value4'], null);
expect(manager.updateScheduled, isFalse);
});
test('child bucket values', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rootRawData = _createRawDataSet();
final Object debugOwner = Object();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rootRawData);
final RestorationBucket child = RestorationBucket.child(
restorationId: 'child1',
parent: root,
debugOwner: debugOwner,
);
expect(child.restorationId, 'child1');
expect(child.debugOwner, debugOwner);
// Bucket contains expected values from rawData.
expect(child.read<int>('foo'), 22);
expect(child.read<String>('bar'), isNull); // Does not exist.
expect(manager.updateScheduled, isFalse);
// Can overwrite existing value.
child.write<int>('foo', 44);
expect(manager.updateScheduled, isTrue);
expect(child.read<int>('foo'), 44);
manager.doSerialization();
expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 44);
expect(manager.updateScheduled, isFalse);
// Can add a new value.
child.write<bool>('value3', true);
expect(manager.updateScheduled, isTrue);
expect(child.read<bool>('value3'), true);
manager.doSerialization();
expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['value3'], true);
expect(manager.updateScheduled, isFalse);
// Can remove existing value.
expect(child.remove<int>('foo'), 44);
expect(manager.updateScheduled, isTrue);
expect(child.read<int>('foo'), isNull); // Does not exist anymore.
manager.doSerialization();
expect(rootRawData[childrenMapKey]['child1'].containsKey('foo'), isFalse);
expect(manager.updateScheduled, isFalse);
// Removing non-existing value is no-op.
expect(child.remove<Object>('value4'), isNull);
expect(manager.updateScheduled, isFalse);
// Can store null.
child.write<bool>('value4', null);
expect(manager.updateScheduled, isTrue);
expect(child.read<int>('value4'), null);
manager.doSerialization();
expect(rootRawData[childrenMapKey]['child1'][valuesMapKey].containsKey('value4'), isTrue);
expect(rootRawData[childrenMapKey]['child1'][valuesMapKey]['value4'], null);
expect(manager.updateScheduled, isFalse);
});
test('claim child with exisiting data', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
final Object debugOwner = Object();
final RestorationBucket child = bucket.claimChild('child1', debugOwner: debugOwner);
expect(manager.updateScheduled, isFalse);
expect(child.restorationId, 'child1');
expect(child.debugOwner, debugOwner);
expect(child.read<int>('foo'), 22);
child.write('bar', 44);
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['bar'], 44);
expect(manager.updateScheduled, isFalse);
});
test('claim child with no exisiting data', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData[childrenMapKey].containsKey('child2'), isFalse);
final Object debugOwner = Object();
final RestorationBucket child = bucket.claimChild('child2', debugOwner: debugOwner);
expect(manager.updateScheduled, isTrue);
expect(child.restorationId, 'child2');
expect(child.debugOwner, debugOwner);
child.write('foo', 55);
expect(child.read<int>('foo'), 55);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('child2'), isTrue);
expect(rawData[childrenMapKey]['child2'][valuesMapKey]['foo'], 55);
});
test('claim child that is already claimed throws if not given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
expect(manager.updateScheduled, isFalse);
expect(child1.restorationId, 'child1');
expect(child1.read<int>('foo'), 22);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
expect(child2.restorationId, 'child1');
expect(child2.read<int>('foo'), isNull); // Value does not exist in this child.
// child1 is not given up before running finalizers.
try {
manager.doSerialization();
fail('expected error');
} on FlutterError catch (e) {
expect(
e.message,
'Multiple owners claimed child RestorationBuckets with the same IDs.\n'
'The following IDs were claimed multiple times from the parent RestorationBucket(restorationId: root, owner: MockManager):\n'
' * "child1" was claimed by:\n'
' * SecondClaim\n'
' * FirstClaim (current owner)'
);
}
});
test('claim child that is already claimed does not throw if given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
expect(manager.updateScheduled, isFalse);
expect(child1.restorationId, 'child1');
expect(child1.read<int>('foo'), 22);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
expect(child2.restorationId, 'child1');
expect(child2.read<int>('foo'), isNull); // Value does not exist in this child.
child2.write<int>('bar', 55);
// give up child1.
child1.dispose();
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey]['child1'][valuesMapKey].containsKey('foo'), isFalse);
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['bar'], 55);
});
test('claiming a claimed child twice and only giving it up once throws', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
expect(child1.restorationId, 'child1');
final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
expect(child2.restorationId, 'child1');
child1.dispose();
final RestorationBucket child3 = bucket.claimChild('child1', debugOwner: 'ThirdClaim');
expect(child3.restorationId, 'child1');
expect(manager.updateScheduled, isTrue);
expect(() => manager.doSerialization(), throwsFlutterError);
});
test('unclaiming and then claiming same id gives fresh bucket', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket bucket = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = bucket.claimChild('child1', debugOwner: 'FirstClaim');
expect(manager.updateScheduled, isFalse);
expect(child1.read<int>('foo'), 22);
child1.dispose();
expect(manager.updateScheduled, isTrue);
final RestorationBucket child2 = bucket.claimChild('child1', debugOwner: 'SecondClaim');
expect(child2.read<int>('foo'), isNull);
});
test('cleans up raw data if last value/child is dropped', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData.containsKey(childrenMapKey), isTrue);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner');
child.dispose();
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(rawData.containsKey(childrenMapKey), isFalse);
expect(rawData.containsKey(valuesMapKey), isTrue);
expect(root.remove<int>('value1'), 10);
expect(root.remove<String>('value2'), 'Hello');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(rawData.containsKey(valuesMapKey), isFalse);
});
test('dispose deletes data', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
child1.claimChild('child1OfChild1', debugOwner: 'owner1.1');
child1.claimChild('child2OfChild1', debugOwner: 'owner1.2');
final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner2');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
expect(rawData[childrenMapKey].containsKey('child2'), isTrue);
child1.dispose();
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
child2.dispose();
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData.containsKey(childrenMapKey), isFalse);
});
test('rename is no-op if same id', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
expect(manager.updateScheduled, isFalse);
expect(child.restorationId, 'child1');
child.rename('child1');
expect(manager.updateScheduled, isFalse);
expect(child.restorationId, 'child1');
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
});
test('rename to unused id', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
final Object rawChildData = rawData[childrenMapKey]['child1'];
expect(rawChildData, isNotNull);
expect(manager.updateScheduled, isFalse);
expect(child.restorationId, 'child1');
child.rename('new-name');
expect(child.restorationId, 'new-name');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
expect(rawData[childrenMapKey]['new-name'], rawChildData);
});
test('rename to used id throws if id is not given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
manager.doSerialization();
expect(child1.restorationId, 'child1');
expect(child2.restorationId, 'child2');
child2.rename('child1');
expect(child2.restorationId, 'child1');
expect(manager.updateScheduled, isTrue);
expect(() => manager.doSerialization(), throwsFlutterError);
});
test('rename to used id does not throw if id is given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
manager.doSerialization();
final Object rawChild1Data = rawData[childrenMapKey]['child1'];
expect(rawChild1Data, isNotNull);
final Object rawChild2Data = rawData[childrenMapKey]['child2'];
expect(rawChild2Data, isNotNull);
expect(child1.restorationId, 'child1');
expect(child2.restorationId, 'child2');
child2.rename('child1');
expect(child2.restorationId, 'child1');
expect(child1.restorationId, 'child1');
child1.dispose();
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey]['child1'], rawChild2Data);
expect(rawData[childrenMapKey].containsKey('child2'), isFalse);
});
test('renaming a to be added child', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final Object rawChild1Data = rawData[childrenMapKey]['child1'];
expect(rawChild1Data, isNotNull);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket child2 = root.claimChild('child1', debugOwner: 'owner1');
child2.rename('foo');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(child1.restorationId, 'child1');
expect(child2.restorationId, 'foo');
expect(rawData[childrenMapKey]['child1'], rawChild1Data);
expect(rawData[childrenMapKey]['foo'], isEmpty); // new bucket
});
test('adopt is no-op if same parent', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
root.adoptChild(child1);
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
});
test('adopt fresh child', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = RestorationBucket.empty(restorationId: 'fresh-child', debugOwner: 'owner1');
root.adoptChild(child);
expect(manager.updateScheduled, isTrue);
child.write('value', 22);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey].containsKey('fresh-child'), isTrue);
expect(rawData[childrenMapKey]['fresh-child'][valuesMapKey]['value'], 22);
child.write('bar', 'blabla');
expect(manager.updateScheduled, isTrue);
});
test('adopt child that already had a parent', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket childOfChild = child.claimChild('childOfChild', debugOwner: 'owner2');
childOfChild.write<String>('foo', 'bar');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
final Object childOfChildData = rawData[childrenMapKey]['child1'][childrenMapKey]['childOfChild'];
expect(childOfChildData, isNotEmpty);
root.adoptChild(childOfChild);
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey]['child1'].containsKey(childrenMapKey), isFalse); // child1 has no children anymore.
expect(rawData[childrenMapKey]['childOfChild'], childOfChildData);
});
test('adopting child throws if id is already in use and not given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket childOfChild = child.claimChild('child1', debugOwner: 'owner2');
childOfChild.write<String>('foo', 'bar');
root.adoptChild(childOfChild);
expect(manager.updateScheduled, isTrue);
expect(() => manager.doSerialization(), throwsFlutterError);
});
test('adopting child does not throw if id is already in use and given up', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket childOfChild = child.claimChild('child1', debugOwner: 'owner2');
childOfChild.write<String>('foo', 'bar');
final Object childOfChildData = rawData[childrenMapKey]['child1'][childrenMapKey]['child1'];
expect(childOfChildData, isNotEmpty);
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
root.adoptChild(childOfChild);
expect(manager.updateScheduled, isTrue);
child.dispose();
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey]['child1'], childOfChildData);
});
test('adopting a to-be-added child under an already in use id', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
final RestorationBucket child1OfChild1 = child1.claimChild('child2', debugOwner: 'owner2');
child1OfChild1.write<String>('hello', 'world');
final RestorationBucket child2OfChild1 = child1.claimChild('child2', debugOwner: 'owner2');
child2OfChild1.write<String>('foo', 'bar');
root.adoptChild(child2OfChild1);
child2.dispose();
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
expect(rawData[childrenMapKey]['child2'][valuesMapKey]['foo'], 'bar');
expect(rawData[childrenMapKey]['child1'][childrenMapKey]['child2'][valuesMapKey]['hello'], 'world');
});
test('decommission drops itself from parent and notifies all listeners', () {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final RestorationBucket child1 = root.claimChild('child1', debugOwner: 'owner1');
final RestorationBucket child2 = root.claimChild('child2', debugOwner: 'owner1');
final RestorationBucket childOfChild1 = child1.claimChild('child1.1', debugOwner: 'owner1');
final RestorationBucket childOfChildOfChild1 = childOfChild1.claimChild('child1.1.1', debugOwner: 'owner1');
expect(manager.updateScheduled, isTrue);
manager.doSerialization();
expect(manager.updateScheduled, isFalse);
bool rootDecommissioned = false;
root.addListener(() {
rootDecommissioned = true;
});
bool child1Decommissioned = false;
child1.addListener(() {
child1Decommissioned = true;
});
bool child2Decommissioned = false;
child2.addListener(() {
child2Decommissioned = true;
});
bool childOfChild1Decommissioned = false;
childOfChild1.addListener(() {
childOfChild1Decommissioned = true;
});
bool childOfChildOfChild1Decommissioned = false;
childOfChildOfChild1.addListener(() {
childOfChildOfChild1Decommissioned = true;
});
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
child1.decommission();
expect(rootDecommissioned, isFalse);
expect(child2Decommissioned, isFalse);
expect(child1Decommissioned, isTrue);
expect(childOfChild1Decommissioned, isTrue);
expect(childOfChildOfChild1Decommissioned, isTrue);
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
});
test('throws when used after dispose', () {
final RestorationBucket bucket = RestorationBucket.empty(restorationId: 'foo', debugOwner: null);
bucket.dispose();
expect(() => bucket.debugOwner, throwsFlutterError);
expect(() => bucket.restorationId, throwsFlutterError);
expect(() => bucket.decommission(), throwsFlutterError);
expect(() => bucket.read<int>('foo'), throwsFlutterError);
expect(() => bucket.write('foo', 10), throwsFlutterError);
expect(() => bucket.remove<int>('foo'), throwsFlutterError);
expect(() => bucket.contains('foo'), throwsFlutterError);
expect(() => bucket.claimChild('child', debugOwner: null), throwsFlutterError);
final RestorationBucket child = RestorationBucket.empty(restorationId: 'child', debugOwner: null);
expect(() => bucket.adoptChild(child), throwsFlutterError);
expect(() => bucket.rename('bar'), throwsFlutterError);
expect(() => bucket.dispose(), throwsFlutterError);
});
test('cannot serialize without manager', () {
final RestorationBucket bucket = RestorationBucket.empty(restorationId: 'foo', debugOwner: null);
expect(() => bucket.write('foo', 10), throwsAssertionError);
});
}
Map<String, dynamic> _createRawDataSet() {
return <String, dynamic>{
valuesMapKey: <String, dynamic>{
'value1' : 10,
'value2' : 'Hello',
},
childrenMapKey: <String, dynamic>{
'child1' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'foo': 22,
},
},
},
};
}
// 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 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
group('RestorationManager', () {
testWidgets('root bucket retrieval', (WidgetTester tester) async {
final List<MethodCall> callsToEngine = <MethodCall>[];
final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) {
callsToEngine.add(call);
return result.future;
});
final RestorationManager manager = RestorationManager();
final Future<RestorationBucket> rootBucketFuture = manager.rootBucket;
RestorationBucket rootBucket;
rootBucketFuture.then((RestorationBucket bucket) {
rootBucket = bucket;
});
expect(rootBucketFuture, isNotNull);
expect(rootBucket, isNull);
// Accessing rootBucket again gives same future.
expect(manager.rootBucket, same(rootBucketFuture));
// Engine has only been contacted once.
expect(callsToEngine, hasLength(1));
expect(callsToEngine.single.method, 'get');
// Complete the engine request.
result.complete(_createEncodedRestorationData1());
await tester.pump();
// Root bucket future completed.
expect(rootBucket, isNotNull);
// Root bucket contains the expected data.
expect(rootBucket.read<int>('value1'), 10);
expect(rootBucket.read<String>('value2'), 'Hello');
final RestorationBucket child = rootBucket.claimChild('child1', debugOwner: null);
expect(child.read<int>('another value'), 22);
// Accessing the root bucket again completes synchronously with same bucket.
RestorationBucket synchronousBucket;
manager.rootBucket.then((RestorationBucket bucket) {
synchronousBucket = bucket;
});
expect(synchronousBucket, isNotNull);
expect(synchronousBucket, same(rootBucket));
});
testWidgets('root bucket received from engine before retrieval', (WidgetTester tester) async {
SystemChannels.restoration.setMethodCallHandler(null);
final List<MethodCall> callsToEngine = <MethodCall>[];
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) {
callsToEngine.add(call);
return null;
});
final RestorationManager manager = RestorationManager();
await _pushDataFromEngine(_createEncodedRestorationData1());
RestorationBucket rootBucket;
manager.rootBucket.then((RestorationBucket bucket) => rootBucket = bucket);
// Root bucket is available synchronously.
expect(rootBucket, isNotNull);
// Engine was never asked.
expect(callsToEngine, isEmpty);
});
testWidgets('root bucket received while engine retrieval is pending', (WidgetTester tester) async {
SystemChannels.restoration.setMethodCallHandler(null);
final List<MethodCall> callsToEngine = <MethodCall>[];
final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) {
callsToEngine.add(call);
return result.future;
});
final RestorationManager manager = RestorationManager();
RestorationBucket rootBucket;
manager.rootBucket.then((RestorationBucket bucket) => rootBucket = bucket);
expect(rootBucket, isNull);
expect(callsToEngine.single.method, 'get');
await _pushDataFromEngine(_createEncodedRestorationData1());
expect(rootBucket, isNotNull);
expect(rootBucket.read<int>('value1'), 10);
result.complete(_createEncodedRestorationData2());
await tester.pump();
RestorationBucket rootBucket2;
manager.rootBucket.then((RestorationBucket bucket) => rootBucket2 = bucket);
expect(rootBucket2, isNotNull);
expect(rootBucket2, same(rootBucket));
expect(rootBucket2.read<int>('value1'), 10);
expect(rootBucket2.contains('foo'), isFalse);
});
testWidgets('root bucket is properly replaced when new data is available', (WidgetTester tester) async {
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) async {
return _createEncodedRestorationData1();
});
final RestorationManager manager = RestorationManager();
RestorationBucket rootBucket;
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket = bucket;
});
await tester.pump();
expect(rootBucket, isNotNull);
expect(rootBucket.read<int>('value1'), 10);
final RestorationBucket child = rootBucket.claimChild('child1', debugOwner: null);
expect(child.read<int>('another value'), 22);
bool rootDecommissioned = false;
bool childDecommissioned = false;
RestorationBucket newRoot;
rootBucket.addListener(() {
rootDecommissioned = true;
manager.rootBucket.then((RestorationBucket bucket) {
newRoot = bucket;
});
// The new bucket is available synchronously.
expect(newRoot, isNotNull);
});
child.addListener(() {
childDecommissioned = true;
});
// Send new Data.
await _pushDataFromEngine(_createEncodedRestorationData2());
expect(rootDecommissioned, isTrue);
expect(childDecommissioned, isTrue);
expect(newRoot, isNot(same(rootBucket)));
child.dispose();
expect(newRoot.read<int>('foo'), 33);
expect(newRoot.read<int>('value1'), null);
final RestorationBucket newChild = newRoot.claimChild('childFoo', debugOwner: null);
expect(newChild.read<String>('bar'), 'Hello');
});
testWidgets('returns null as root bucket when restoration is disabled', (WidgetTester tester) async {
final List<MethodCall> callsToEngine = <MethodCall>[];
final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) {
callsToEngine.add(call);
return result.future;
});
int listenerCount = 0;
final RestorationManager manager = RestorationManager()..addListener(() {
listenerCount++;
});
RestorationBucket rootBucket;
bool rootBucketResolved = false;
manager.rootBucket.then((RestorationBucket bucket) {
rootBucketResolved = true;
rootBucket = bucket;
});
await tester.pump();
expect(rootBucketResolved, isFalse);
expect(listenerCount, 0);
result.complete(_packageRestorationData(enabled: false));
await tester.pump();
expect(rootBucketResolved, isTrue);
expect(rootBucket, isNull);
// Switch to non-null.
await _pushDataFromEngine(_createEncodedRestorationData1());
expect(listenerCount, 1);
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket = bucket;
});
expect(rootBucket, isNotNull);
// Switch to null again.
await _pushDataFromEngine(_packageRestorationData(enabled: false));
expect(listenerCount, 2);
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket = bucket;
});
expect(rootBucket, isNull);
});
});
test('debugIsSerializableForRestoration', () {
expect(debugIsSerializableForRestoration(Object()), isFalse);
expect(debugIsSerializableForRestoration(Container()), isFalse);
expect(debugIsSerializableForRestoration(null), isTrue);
expect(debugIsSerializableForRestoration(147823), isTrue);
expect(debugIsSerializableForRestoration(12.43), isTrue);
expect(debugIsSerializableForRestoration('Hello World'), isTrue);
expect(debugIsSerializableForRestoration(<int>[12, 13, 14]), isTrue);
expect(debugIsSerializableForRestoration(<String, int>{'v1' : 10, 'v2' : 23}), isTrue);
expect(debugIsSerializableForRestoration(<String, dynamic>{
'hello': <int>[12, 12, 12],
'world': <int, bool>{
1: true,
2: false,
4: true,
},
}), isTrue);
});
}
Future<void> _pushDataFromEngine(Map<dynamic, dynamic> data) async {
await ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
'flutter/restoration',
const StandardMethodCodec().encodeMethodCall(MethodCall('push', data)),
(_) { },
);
}
Map<dynamic, dynamic> _createEncodedRestorationData1() {
final Map<String, dynamic> data = <String, dynamic>{
valuesMapKey: <String, dynamic>{
'value1' : 10,
'value2' : 'Hello',
},
childrenMapKey: <String, dynamic>{
'child1' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'another value': 22,
}
},
},
};
return _packageRestorationData(data: data);
}
Map<dynamic, dynamic> _createEncodedRestorationData2() {
final Map<String, dynamic> data = <String, dynamic>{
valuesMapKey: <String, dynamic>{
'foo' : 33,
},
childrenMapKey: <String, dynamic>{
'childFoo' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'bar': 'Hello',
}
},
},
};
return _packageRestorationData(data: data);
}
Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic, dynamic> data}) {
final ByteData encoded = const StandardMessageCodec().encodeMessage(data);
return <dynamic, dynamic>{
'enabled': enabled,
'data': encoded == null ? null : encoded.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes)
};
}
// 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/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('value is not accessible when not registered', (WidgetTester tester) async {
expect(() => RestorableNum<num>(0).value, throwsAssertionError);
expect(() => RestorableDouble(1.0).value, throwsAssertionError);
expect(() => RestorableInt(1).value, throwsAssertionError);
expect(() => RestorableString('hello').value, throwsAssertionError);
expect(() => RestorableBool(true).value, throwsAssertionError);
expect(() => RestorableTextEditingController().value, throwsAssertionError);
expect(() => _TestRestorableValue().value, throwsAssertionError);
});
testWidgets('cannot initialize with null', (WidgetTester tester) async {
expect(() => RestorableNum<num>(null), throwsAssertionError);
expect(() => RestorableDouble(null), throwsAssertionError);
expect(() => RestorableInt(null), throwsAssertionError);
expect(() => RestorableString(null).value, throwsAssertionError);
expect(() => RestorableBool(null).value, throwsAssertionError);
});
testWidgets('work when not in restoration scope', (WidgetTester tester) async {
await tester.pumpWidget(const _RestorableWidget());
expect(find.text('hello world'), findsOneWidget);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Initialized to default values.
expect(state.numValue.value, 99);
expect(state.doubleValue.value, 123.2);
expect(state.intValue.value, 42);
expect(state.stringValue.value, 'hello world');
expect(state.boolValue.value, false);
expect(state.controllerValue.value.text, 'FooBar');
expect(state.objectValue.value, 55);
// Modify values.
state.setProperties(() {
state.numValue.value = 42.2;
state.doubleValue.value = 441.3;
state.intValue.value = 10;
state.stringValue.value = 'guten tag';
state.boolValue.value = true;
state.controllerValue.value.text = 'blabla';
state.objectValue.value = 53;
});
await tester.pump();
expect(state.numValue.value, 42.2);
expect(state.doubleValue.value, 441.3);
expect(state.intValue.value, 10);
expect(state.stringValue.value, 'guten tag');
expect(state.boolValue.value, true);
expect(state.controllerValue.value.text, 'blabla');
expect(state.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
});
testWidgets('restart and restore', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
expect(find.text('hello world'), findsOneWidget);
_RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Initialized to default values.
expect(state.numValue.value, 99);
expect(state.doubleValue.value, 123.2);
expect(state.intValue.value, 42);
expect(state.stringValue.value, 'hello world');
expect(state.boolValue.value, false);
expect(state.controllerValue.value.text, 'FooBar');
expect(state.objectValue.value, 55);
// Modify values.
state.setProperties(() {
state.numValue.value = 42.2;
state.doubleValue.value = 441.3;
state.intValue.value = 10;
state.stringValue.value = 'guten tag';
state.boolValue.value = true;
state.controllerValue.value.text = 'blabla';
state.objectValue.value = 53;
});
await tester.pump();
expect(state.numValue.value, 42.2);
expect(state.doubleValue.value, 441.3);
expect(state.intValue.value, 10);
expect(state.stringValue.value, 'guten tag');
expect(state.boolValue.value, true);
expect(state.controllerValue.value.text, 'blabla');
expect(state.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
// Restores to previous values.
await tester.restartAndRestore();
final _RestorableWidgetState oldState = state;
state = tester.state(find.byType(_RestorableWidget));
expect(state, isNot(same(oldState)));
expect(state.numValue.value, 42.2);
expect(state.doubleValue.value, 441.3);
expect(state.intValue.value, 10);
expect(state.stringValue.value, 'guten tag');
expect(state.boolValue.value, true);
expect(state.controllerValue.value.text, 'blabla');
expect(state.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
});
testWidgets('cannot set to null', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
expect(find.text('hello world'), findsOneWidget);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
expect(() => state.numValue.value = null, throwsAssertionError);
expect(() => state.doubleValue.value = null, throwsAssertionError);
expect(() => state.intValue.value = null, throwsAssertionError);
expect(() => state.stringValue.value = null, throwsAssertionError);
expect(() => state.boolValue.value = null, throwsAssertionError);
});
testWidgets('restore to older state', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
expect(find.text('hello world'), findsOneWidget);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
// Modify values.
state.setProperties(() {
state.numValue.value = 42.2;
state.doubleValue.value = 441.3;
state.intValue.value = 10;
state.stringValue.value = 'guten tag';
state.boolValue.value = true;
state.controllerValue.value.text = 'blabla';
state.objectValue.value = 53;
});
await tester.pump();
expect(find.text('guten tag'), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
// Modify values.
state.setProperties(() {
state.numValue.value = 20;
state.doubleValue.value = 20.0;
state.intValue.value = 20;
state.stringValue.value = 'ciao';
state.boolValue.value = false;
state.controllerValue.value.text = 'blub';
state.objectValue.value = 20;
});
await tester.pump();
expect(find.text('ciao'), findsOneWidget);
final TextEditingController controller = state.controllerValue.value;
// Restore to previous.
await tester.restoreFrom(restorationData);
expect(state.numValue.value, 42.2);
expect(state.doubleValue.value, 441.3);
expect(state.intValue.value, 10);
expect(state.stringValue.value, 'guten tag');
expect(state.boolValue.value, true);
expect(state.controllerValue.value.text, 'blabla');
expect(state.objectValue.value, 53);
expect(find.text('guten tag'), findsOneWidget);
expect(state.controllerValue.value, isNot(same(controller)));
// Restore to empty data will re-initialize to default values.
await tester.restoreFrom(TestRestorationData.empty);
expect(state.numValue.value, 99);
expect(state.doubleValue.value, 123.2);
expect(state.intValue.value, 42);
expect(state.stringValue.value, 'hello world');
expect(state.boolValue.value, false);
expect(state.controllerValue.value.text, 'FooBar');
expect(state.objectValue.value, 55);
expect(find.text('hello world'), findsOneWidget);
});
testWidgets('call notifiers when value changes', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
expect(find.text('hello world'), findsOneWidget);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
final List<String> notifyLog = <String>[];
state.numValue.addListener(() {
notifyLog.add('num');
});
state.doubleValue.addListener(() {
notifyLog.add('double');
});
state.intValue.addListener(() {
notifyLog.add('int');
});
state.stringValue.addListener(() {
notifyLog.add('string');
});
state.boolValue.addListener(() {
notifyLog.add('bool');
});
state.controllerValue.addListener(() {
notifyLog.add('controller');
});
state.objectValue.addListener(() {
notifyLog.add('object');
});
state.setProperties(() {
state.numValue.value = 42.2;
});
expect(notifyLog.single, 'num');
notifyLog.clear();
state.setProperties(() {
state.doubleValue.value = 42.2;
});
expect(notifyLog.single, 'double');
notifyLog.clear();
state.setProperties(() {
state.intValue.value = 45;
});
expect(notifyLog.single, 'int');
notifyLog.clear();
state.setProperties(() {
state.stringValue.value = 'bar';
});
expect(notifyLog.single, 'string');
notifyLog.clear();
state.setProperties(() {
state.boolValue.value = true;
});
expect(notifyLog.single, 'bool');
notifyLog.clear();
state.setProperties(() {
state.controllerValue.value.text = 'foo';
});
expect(notifyLog.single, 'controller');
notifyLog.clear();
state.setProperties(() {
state.objectValue.value = 42;
});
expect(notifyLog.single, 'object');
notifyLog.clear();
await tester.pump();
expect(find.text('bar'), findsOneWidget);
// Does not notify when set to same value.
state.setProperties(() {
state.numValue.value = 42.2;
state.doubleValue.value = 42.2;
state.intValue.value = 45;
state.stringValue.value = 'bar';
state.boolValue.value = true;
state.controllerValue.value.text = 'foo';
});
expect(notifyLog, isEmpty);
});
testWidgets('RestorableValue calls didUpdateValue', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(),
));
expect(find.text('hello world'), findsOneWidget);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
expect(state.objectValue.didUpdateValueCallCount, 0);
state.setProperties(() {
state.objectValue.value = 44;
});
expect(state.objectValue.didUpdateValueCallCount, 1);
await tester.pump();
state.setProperties(() {
state.objectValue.value = 44;
});
expect(state.objectValue.didUpdateValueCallCount, 1);
});
}
class _TestRestorableValue extends RestorableValue<Object> {
@override
Object createDefaultValue() {
return 55;
}
int didUpdateValueCallCount = 0;
@override
void didUpdateValue(Object oldValue) {
didUpdateValueCallCount++;
notifyListeners();
}
@override
Object fromPrimitives(Object data) {
return data;
}
@override
Object toPrimitives() {
return value;
}
}
class _RestorableWidget extends StatefulWidget {
const _RestorableWidget({Key key}) : super(key: key);
@override
State<_RestorableWidget> createState() => _RestorableWidgetState();
}
class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMixin {
final RestorableNum<num> numValue = RestorableNum<num>(99);
final RestorableDouble doubleValue = RestorableDouble(123.2);
final RestorableInt intValue = RestorableInt(42);
final RestorableString stringValue = RestorableString('hello world');
final RestorableBool boolValue = RestorableBool(false);
final RestorableTextEditingController controllerValue = RestorableTextEditingController(text: 'FooBar');
final _TestRestorableValue objectValue = _TestRestorableValue();
@override
void restoreState(RestorationBucket oldBucket) {
registerForRestoration(numValue, 'num');
registerForRestoration(doubleValue, 'double');
registerForRestoration(intValue, 'int');
registerForRestoration(stringValue, 'string');
registerForRestoration(boolValue,'bool');
registerForRestoration(controllerValue, 'controller');
registerForRestoration(objectValue, 'object');
}
void setProperties(VoidCallback callback) {
setState(callback);
}
@override
Widget build(BuildContext context) {
return Text(stringValue.value ?? 'null', textDirection: TextDirection.ltr,);
}
@override
String get restorationId => 'widget';
}
// 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/services.dart';
import 'package:flutter/widgets.dart';
export '../services/restoration.dart';
class BucketSpy extends StatefulWidget {
const BucketSpy({Key key, this.child}) : super(key: key);
final Widget child;
@override
State<BucketSpy> createState() => BucketSpyState();
}
class BucketSpyState extends State<BucketSpy> {
RestorationBucket bucket;
@override
void didChangeDependencies() {
super.didChangeDependencies();
bucket = RestorationScope.of(context);
}
@override
Widget build(BuildContext context) {
return widget.child ?? Container();
}
}
// 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/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
testWidgets('claims bucket', (WidgetTester tester) async {
const String id = 'hello world 1234';
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData, isEmpty);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: id,
),
),
);
manager.doSerialization();
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, id);
expect(rawData[childrenMapKey].containsKey(id), isTrue);
expect(state.property.value, 10);
expect(rawData[childrenMapKey][id][valuesMapKey]['foo'], 10);
expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
expect(state.toogleBucketLog, isEmpty);
expect(state.restoreStateLog.single, isNull);
});
testWidgets('claimed bucket with data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, 'child1');
expect(state.property.value, 22);
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
expect(state.toogleBucketLog, isEmpty);
expect(state.restoreStateLog.single, isNull);
});
testWidgets('renames existing bucket when new ID is provided via widget', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
// Claimed existing bucket with data.
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), 22);
final RestorationBucket bucket = state.bucket;
state.property.log.clear();
state.restoreStateLog.clear();
// Rename the existing bucket.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'something else',
),
),
);
manager.doSerialization();
expect(state.bucket.restorationId, 'something else');
expect(state.bucket.read<int>('foo'), 22);
expect(state.bucket, same(bucket));
expect(state.property.log, isEmpty);
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog, isEmpty);
});
testWidgets('renames existing bucket when didUpdateRestorationId is called', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
// Claimed existing bucket with data.
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), 22);
final RestorationBucket bucket = state.bucket;
state.property.log.clear();
state.restoreStateLog.clear();
// Rename the existing bucket.
state.injectId('newnewnew');
manager.doSerialization();
expect(state.bucket.restorationId, 'newnewnew');
expect(state.bucket.read<int>('foo'), 22);
expect(state.bucket, same(bucket));
expect(state.property.log, isEmpty);
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog, isEmpty);
});
testWidgets('Disposing widget removes its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Container(),
),
);
manager.doSerialization();
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
});
testWidgets('toggling id between null and non-null', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: null,
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket, isNull);
expect(state.property.value, 10); // Initialized to default.
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 22);
expect(state.property.log, <String>['createDefaultValue', 'initWithValue']);
state.property.log.clear();
expect(state.restoreStateLog.single, isNull);
expect(state.toogleBucketLog, isEmpty);
state.restoreStateLog.clear();
state.toogleBucketLog.clear();
// Change id to non-null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket.restorationId, 'child1');
expect(state.property.value, 10);
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 10);
expect(state.property.log, <String>['toPrimitives']);
state.property.log.clear();
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog.single, isNull);
state.restoreStateLog.clear();
state.toogleBucketLog.clear();
final RestorationBucket bucket = state.bucket;
// Change id back to null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: null,
),
),
);
manager.doSerialization();
expect(state.bucket, isNull);
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
expect(state.property.log, isEmpty);
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog.single, same(bucket));
});
testWidgets('move in and out of scope', (WidgetTester tester) async {
final Key key = GlobalKey();
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
await tester.pumpWidget(
_TestRestorableWidget(
key: key,
restorationId: 'child1',
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket, isNull);
expect(state.property.value, 10); // Initialized to default.
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 22);
expect(state.property.log, <String>['createDefaultValue', 'initWithValue']);
state.property.log.clear();
expect(state.restoreStateLog.single, isNull);
expect(state.toogleBucketLog, isEmpty);
state.restoreStateLog.clear();
state.toogleBucketLog.clear();
// Move it under a valid scope.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: _TestRestorableWidget(
key: key,
restorationId: 'child1',
),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket.restorationId, 'child1');
expect(state.property.value, 10);
expect(rawData[childrenMapKey]['child1'][valuesMapKey]['foo'], 10);
expect(state.property.log, <String>['toPrimitives']);
state.property.log.clear();
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog.single, isNull);
state.restoreStateLog.clear();
state.toogleBucketLog.clear();
final RestorationBucket bucket = state.bucket;
// Move out of scope again.
await tester.pumpWidget(
_TestRestorableWidget(
key: key,
restorationId: 'child1',
),
);
manager.doSerialization();
expect(state.bucket, isNull);
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
expect(state.property.log, isEmpty);
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog.single, same(bucket));
});
testWidgets('moving scope moves its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final Key key = GlobalKey();
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(
restorationId: 'fixed',
child: _TestRestorableWidget(
key: key,
restorationId: 'moving-child',
),
),
],
),
),
);
manager.doSerialization();
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, 'moving-child');
expect(rawData[childrenMapKey]['fixed'][childrenMapKey].containsKey('moving-child'), isTrue);
final RestorationBucket bucket = state.bucket;
state.property.log.clear();
state.restoreStateLog.clear();
state.bucket.write('value', 11);
manager.doSerialization();
// Move widget.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(
restorationId: 'fixed',
child: Container(),
),
_TestRestorableWidget(
key: key,
restorationId: 'moving-child',
),
],
),
),
);
manager.doSerialization();
expect(state.bucket.restorationId, 'moving-child');
expect(state.bucket, same(bucket));
expect(state.bucket.read<int>('value'), 11);
expect(state.property.log, isEmpty);
expect(state.toogleBucketLog, isEmpty);
expect(state.restoreStateLog, isEmpty);
expect(rawData[childrenMapKey]['fixed'], isEmpty);
expect(rawData[childrenMapKey].containsKey('moving-child'), isTrue);
});
testWidgets('decommission claims new bucket with data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
manager.doSerialization();
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket.restorationId, 'child1');
expect(state.property.value, 10); // Initialized to default.
expect(state.bucket.read<int>('foo'), 10);
final RestorationBucket bucket = state.bucket;
state.property.log.clear();
state.restoreStateLog.clear();
// Replace root bucket.
root..decommission()..dispose();
root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const _TestRestorableWidget(
restorationId: 'child1',
),
),
);
// Bucket has been replaced.
expect(state.bucket, isNot(same(bucket)));
expect(state.bucket.restorationId, 'child1');
expect(state.property.value, 22); // Restored value.
expect(state.bucket.read<int>('foo'), 22);
expect(state.restoreStateLog.single, bucket);
expect(state.toogleBucketLog, isEmpty);
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
});
testWidgets('restartAndRestore', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
)
);
_TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(state.bucket, isNotNull);
expect(state.property.value, 10); // default
expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
expect(state.restoreStateLog.single, isNull);
expect(state.toogleBucketLog, isEmpty);
_clearLogs(state);
state.setProperties(() {
state.property.value = 20;
});
await tester.pump();
expect(state.property.value, 20);
expect(state.property.log, <String>['toPrimitives']);
expect(state.restoreStateLog, isEmpty);
expect(state.toogleBucketLog, isEmpty);
_clearLogs(state);
final _TestRestorableWidgetState oldState = state;
await tester.restartAndRestore();
state = tester.state(find.byType(_TestRestorableWidget));
expect(state, isNot(same(oldState)));
expect(state.property.value, 20);
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
expect(state.restoreStateLog.single, isNull);
expect(state.toogleBucketLog, isEmpty);
});
testWidgets('restore while running', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
_TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.setProperties(() {
state.property.value = 20;
});
await tester.pump();
expect(state.property.value, 20);
final TestRestorationData data = await tester.getRestorationData();
state.setProperties(() {
state.property.value = 30;
});
await tester.pump();
expect(state.property.value, 30);
_clearLogs(state);
final _TestRestorableWidgetState oldState = state;
final RestorationBucket oldBucket = oldState.bucket;
await tester.restoreFrom(data);
state = tester.state(find.byType(_TestRestorableWidget));
expect(state, same(oldState));
expect(state.property.value, 20);
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
expect(state.restoreStateLog.single, oldBucket);
expect(state.toogleBucketLog, isEmpty);
});
testWidgets('can register additional property outside of restoreState', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.registerAdditionalProperty();
expect(state.additionalProperty.value, 11);
expect(state.additionalProperty.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
state.setProperties(() {
state.additionalProperty.value = 33;
});
await tester.pump();
expect(state.additionalProperty.value, 33);
final TestRestorationData data = await tester.getRestorationData();
state.setProperties(() {
state.additionalProperty.value = 44;
});
await tester.pump();
expect(state.additionalProperty.value, 44);
_clearLogs(state);
await tester.restoreFrom(data);
expect(state, same(tester.state(find.byType(_TestRestorableWidget))));
expect(state.additionalProperty.value, 33);
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
});
testWidgets('cannot register same property twice', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.registerAdditionalProperty();
await tester.pump();
expect(() => state.registerAdditionalProperty(), throwsAssertionError);
});
testWidgets('cannot register under ID that is already in use', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
expect(() => state.registerPropertyUnderSameId(), throwsAssertionError);
});
testWidgets('data of disabled property is not stored', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
_TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.setProperties(() {
state.property.value = 30;
});
await tester.pump();
expect(state.property.value, 30);
expect(state.bucket.read<int>('foo'), 30);
_clearLogs(state);
state.setProperties(() {
state.property.enabled = false;
});
await tester.pump();
expect(state.property.value, 30);
expect(state.bucket.contains('foo'), isFalse);
expect(state.property.log, isEmpty);
state.setProperties(() {
state.property.value = 40;
});
await tester.pump();
expect(state.bucket.contains('foo'), isFalse);
expect(state.property.log, isEmpty);
await tester.restartAndRestore();
state = tester.state(find.byType(_TestRestorableWidget));
expect(state.property.log, <String>['createDefaultValue', 'initWithValue', 'toPrimitives']);
expect(state.property.value, 10); // Initialized to default value.
});
testWidgets('Enabling property stores its data again', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
_TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
_clearLogs(state);
state.setProperties(() {
state.property.enabled = false;
});
await tester.pump();
expect(state.bucket.contains('foo'), isFalse);
state.setProperties(() {
state.property.value = 40;
});
await tester.pump();
expect(state.property.value, 40);
expect(state.bucket.contains('foo'), isFalse);
expect(state.property.log, isEmpty);
state.setProperties(() {
state.property.enabled = true;
});
await tester.pump();
expect(state.bucket.read<int>('foo'), 40);
expect(state.property.log, <String>['toPrimitives']);
await tester.restartAndRestore();
state = tester.state(find.byType(_TestRestorableWidget));
expect(state.property.log, <String>['fromPrimitives', 'initWithValue']);
expect(state.property.value, 40);
});
testWidgets('Unregistering a property removes its data', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.registerAdditionalProperty();
await tester.pump();
expect(state.additionalProperty.value, 11);
expect(state.bucket.read<int>('additional'), 11);
state.unregisterAdditionalProperty();
await tester.pump();
expect(state.bucket.contains('additional'), isFalse);
expect(() => state.additionalProperty.value, throwsAssertionError); // No longer registered.
// Can register the same property again.
state.registerAdditionalProperty();
await tester.pump();
expect(state.additionalProperty.value, 11);
expect(state.bucket.read<int>('additional'), 11);
});
testWidgets('Disposing a property unregisters it, but keeps data', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _TestRestorableWidget(
restorationId: 'widget',
),
),
);
final _TestRestorableWidgetState state = tester.state(find.byType(_TestRestorableWidget));
state.registerAdditionalProperty();
await tester.pump();
expect(state.additionalProperty.value, 11);
expect(state.bucket.read<int>('additional'), 11);
state.additionalProperty.dispose();
await tester.pump();
expect(state.bucket.read<int>('additional'), 11);
// Can register property under same id again.
state.additionalProperty = _TestRestorableProperty(22);
state.registerAdditionalProperty();
await tester.pump();
expect(state.additionalProperty.value, 11); // Old value restored.
expect(state.bucket.read<int>('additional'), 11);
});
test('RestorableProperty throws after disposed', () {
final RestorableProperty<Object> property = _TestRestorableProperty(10);
property.dispose();
expect(() => property.dispose(), throwsFlutterError);
});
}
void _clearLogs(_TestRestorableWidgetState state) {
state.property.log.clear();
state.additionalProperty?.log?.clear();
state.restoreStateLog.clear();
state.toogleBucketLog.clear();
}
class _TestRestorableWidget extends StatefulWidget {
const _TestRestorableWidget({Key key, this.restorationId}) : super(key: key);
final String restorationId;
@override
State<_TestRestorableWidget> createState() => _TestRestorableWidgetState();
}
class _TestRestorableWidgetState extends State<_TestRestorableWidget> with RestorationMixin {
final _TestRestorableProperty property = _TestRestorableProperty(10);
_TestRestorableProperty additionalProperty;
bool _rerigisterAdditionalProperty = false;
final List<RestorationBucket> restoreStateLog = <RestorationBucket>[];
final List<RestorationBucket> toogleBucketLog = <RestorationBucket>[];
@override
void restoreState(RestorationBucket oldBucket) {
restoreStateLog.add(oldBucket);
registerForRestoration(property, 'foo');
if (_rerigisterAdditionalProperty && additionalProperty != null) {
registerForRestoration(additionalProperty, 'additional');
}
}
@override
void didToggleBucket(RestorationBucket oldBucket) {
toogleBucketLog.add(oldBucket);
super.didToggleBucket(oldBucket);
}
@override
void dispose() {
super.dispose();
property.dispose();
additionalProperty?.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
void setProperties(VoidCallback fn) => setState(fn);
String _injectedId;
void injectId(String id) {
_injectedId = id;
didUpdateRestorationId();
}
void registerAdditionalProperty({bool reregister = true}) {
additionalProperty ??= _TestRestorableProperty(11);
registerForRestoration(additionalProperty, 'additional');
_rerigisterAdditionalProperty = reregister;
}
void unregisterAdditionalProperty() {
unregisterFromRestoration(additionalProperty);
}
void registerPropertyUnderSameId() {
registerForRestoration(_TestRestorableProperty(11), 'foo');
}
@override
String get restorationId => _injectedId ?? widget.restorationId;
}
Map<String, dynamic> _createRawDataSet() {
return <String, dynamic>{
valuesMapKey: <String, dynamic>{
'value1' : 10,
'value2' : 'Hello',
},
childrenMapKey: <String, dynamic>{
'child1' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'foo': 22,
}
},
'child2' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'bar': 33,
}
},
},
};
}
class _TestRestorableProperty extends RestorableProperty<Object> {
_TestRestorableProperty(this._value);
List<String> log = <String>[];
@override
bool get enabled => _enabled;
bool _enabled = true;
set enabled(bool value) {
_enabled = value;
notifyListeners();
}
@override
Object createDefaultValue() {
log.add('createDefaultValue');
return _value;
}
@override
Object fromPrimitives(Object data) {
log.add('fromPrimitives');
return data;
}
Object get value {
assert(isRegistered);
return _value;
}
Object _value;
set value(Object value) {
_value = value;
notifyListeners();
}
@override
void initWithValue(Object v) {
log.add('initWithValue');
_value = v;
}
@override
Object toPrimitives() {
log.add('toPrimitives');
return _value;
}
}
// 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/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
group('UnmanagedRestorationScope', () {
testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
final RestorationBucket bucket1 = RestorationBucket.empty(
restorationId: 'foo',
debugOwner: 'owner',
);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: bucket1,
child: const BucketSpy(),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, bucket1);
// Notifies when bucket changes.
final RestorationBucket bucket2 = RestorationBucket.empty(
restorationId: 'foo2',
debugOwner: 'owner',
);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: bucket2,
child: const BucketSpy(),
),
);
expect(state.bucket, bucket2);
});
testWidgets('null bucket disables restoration', (WidgetTester tester) async {
await tester.pumpWidget(
const UnmanagedRestorationScope(
bucket: null,
child: BucketSpy(),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
});
});
group('RestorationScope', () {
testWidgets('makes bucket available to descendants', (WidgetTester tester) async {
const String id = 'hello world 1234';
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData, isEmpty);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: id,
child: BucketSpy(),
),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, id);
expect(rawData[childrenMapKey].containsKey(id), isTrue);
});
testWidgets('bucket for descendants contains data claimed from parent', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'child1',
child: BucketSpy(),
),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), 22);
});
testWidgets('renames existing bucket when new ID is provided', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'child1',
child: BucketSpy(),
),
),
);
manager.doSerialization();
// Claimed existing bucket with data.
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), 22);
final RestorationBucket bucket = state.bucket;
// Rename the existing bucket.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'something else',
child: BucketSpy(),
),
),
);
manager.doSerialization();
expect(state.bucket.restorationId, 'something else');
expect(state.bucket.read<int>('foo'), 22);
expect(state.bucket, same(bucket));
});
testWidgets('Disposing a scope removes its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = _createRawDataSet();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'child1',
child: BucketSpy(),
),
),
);
manager.doSerialization();
expect(rawData[childrenMapKey].containsKey('child1'), isTrue);
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Container(),
),
);
manager.doSerialization();
expect(rawData[childrenMapKey].containsKey('child1'), isFalse);
});
testWidgets('no bucket for descendants when id is null', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: null,
child: BucketSpy(),
),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Change id to non-null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'foo',
child: BucketSpy(),
),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket.restorationId, 'foo');
// Change id back to null.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: null,
child: BucketSpy(),
),
),
);
manager.doSerialization();
expect(state.bucket, isNull);
});
testWidgets('no bucket for descendants when scope is null', (WidgetTester tester) async {
final Key scopeKey = GlobalKey();
await tester.pumpWidget(
RestorationScope(
key: scopeKey,
restorationId: 'foo',
child: const BucketSpy(),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Move it under a valid scope.
final MockRestorationManager manager = MockRestorationManager();
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: RestorationScope(
key: scopeKey,
restorationId: 'foo',
child: const BucketSpy(),
),
),
);
manager.doSerialization();
expect(state.bucket, isNotNull);
expect(state.bucket.restorationId, 'foo');
// Move out of scope again.
await tester.pumpWidget(
RestorationScope(
key: scopeKey,
restorationId: 'foo',
child: const BucketSpy(),
),
);
manager.doSerialization();
expect(state.bucket, isNull);
});
testWidgets('no bucket for descendants when scope and id are null', (WidgetTester tester) async {
await tester.pumpWidget(
const RestorationScope(
restorationId: null,
child: BucketSpy(),
),
);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
});
testWidgets('moving scope moves its data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
final Key scopeKey = GlobalKey();
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(
restorationId: 'fixed',
child: RestorationScope(
key: scopeKey,
restorationId: 'moving-child',
child: const BucketSpy(),
),
),
],
),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'moving-child');
expect(rawData[childrenMapKey]['fixed'][childrenMapKey].containsKey('moving-child'), isTrue);
final RestorationBucket bucket = state.bucket;
state.bucket.write('value', 11);
manager.doSerialization();
// Move scope.
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: Row(
textDirection: TextDirection.ltr,
children: <Widget>[
RestorationScope(
restorationId: 'fixed',
child: Container(),
),
RestorationScope(
key: scopeKey,
restorationId: 'moving-child',
child: const BucketSpy(),
),
],
),
),
);
manager.doSerialization();
expect(state.bucket.restorationId, 'moving-child');
expect(state.bucket, same(bucket));
expect(state.bucket.read<int>('value'), 11);
expect(rawData[childrenMapKey]['fixed'], isEmpty);
expect(rawData[childrenMapKey].containsKey('moving-child'), isTrue);
});
testWidgets('decommission claims new bucket with data', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
RestorationBucket root = RestorationBucket.root(manager: manager, rawData: <String, dynamic>{});
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'child1',
child: BucketSpy(),
),
),
);
manager.doSerialization();
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), isNull); // Does not exist.
final RestorationBucket bucket = state.bucket;
// Replace root bucket.
root..decommission()..dispose();
root = RestorationBucket.root(manager: manager, rawData: _createRawDataSet());
await tester.pumpWidget(
UnmanagedRestorationScope(
bucket: root,
child: const RestorationScope(
restorationId: 'child1',
child: BucketSpy(),
),
),
);
// Bucket has been replaced.
expect(state.bucket, isNot(same(bucket)));
expect(state.bucket.restorationId, 'child1');
expect(state.bucket.read<int>('foo'), 22);
});
});
}
Map<String, dynamic> _createRawDataSet() {
return <String, dynamic>{
valuesMapKey: <String, dynamic>{
'value1' : 10,
'value2' : 'Hello',
},
childrenMapKey: <String, dynamic>{
'child1' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'foo': 22,
}
},
'child2' : <String, dynamic>{
valuesMapKey : <String, dynamic>{
'bar': 33,
}
},
},
};
}
// 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 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'restoration.dart';
void main() {
final TestAutomatedTestWidgetsFlutterBinding binding = TestAutomatedTestWidgetsFlutterBinding();
setUp(() {
binding._restorationManager = MockRestorationManager();
});
testWidgets('does not inject root bucket if inside scope', (WidgetTester tester) async {
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: manager, rawData: rawData);
expect(rawData, isEmpty);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: root,
child: const RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
),
);
manager.doSerialization();
expect(binding.restorationManager.rootBucketAccessed, 0);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'root-child');
expect(rawData[childrenMapKey].containsKey('root-child'), isTrue);
expect(find.text('Hello'), findsOneWidget);
});
testWidgets('waits for root bucket', (WidgetTester tester) async {
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
// Child rendering is delayed until root bucket is available.
expect(find.text('Hello'), findsNothing);
expect(binding.firstFrameIsDeferred, isTrue);
// Complete the future.
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData);
bucketCompleter.complete(root);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'root-child');
expect(rawData[childrenMapKey].containsKey('root-child'), isTrue);
});
testWidgets('no delay when root is available synchronously', (WidgetTester tester) async {
final Map<String, dynamic> rawData = <String, dynamic>{};
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: rawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'root-child');
expect(rawData[childrenMapKey].containsKey('root-child'), isTrue);
});
testWidgets('does not insert root when restoration id is null', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: null,
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 0);
expect(find.text('Hello'), findsOneWidget);
expect(binding.firstFrameIsDeferred, isFalse);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
// Change restoration id to non-null.
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull); // root bucket future has not completed yet.
// Complete the future.
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: <String, dynamic>{});
bucketCompleter.complete(root);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket.restorationId, 'root-child');
// Change ID back to null.
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: null,
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull);
});
testWidgets('injects root bucket when moved out of scope', (WidgetTester tester) async {
final Key rootScopeKey = GlobalKey();
final MockRestorationManager manager = MockRestorationManager();
final Map<String, dynamic> inScopeRawData = <String, dynamic>{};
final RestorationBucket inScopeRootBucket = RestorationBucket.root(manager: manager, rawData: inScopeRawData);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: inScopeRootBucket,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 0);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket.restorationId, 'root-child');
expect(inScopeRawData[childrenMapKey].containsKey('root-child'), isTrue);
// Move out of scope.
final Completer<RestorationBucket> bucketCompleter = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = bucketCompleter.future;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final Map<String, dynamic> outOfScopeRawData = <String, dynamic>{};
final RestorationBucket outOfScopeRootBucket = RestorationBucket.root(manager: binding.restorationManager, rawData: outOfScopeRawData);
bucketCompleter.complete(outOfScopeRootBucket);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket.restorationId, 'root-child');
expect(outOfScopeRawData[childrenMapKey].containsKey('root-child'), isTrue);
expect(inScopeRawData, isEmpty);
// Move into scope.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnmanagedRestorationScope(
bucket: inScopeRootBucket,
child: RootRestorationScope(
key: rootScopeKey,
restorationId: 'root-child',
child: const BucketSpy(
child: Text('Hello'),
),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket.restorationId, 'root-child');
expect(outOfScopeRawData, isEmpty);
expect(inScopeRawData[childrenMapKey].containsKey('root-child'), isTrue);
});
testWidgets('injects new root when old one is decommissioned', (WidgetTester tester) async {
final Map<String, dynamic> firstRawData = <String, dynamic>{};
final RestorationBucket firstRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: firstRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(firstRoot);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
state.bucket.write('foo', 42);
expect(firstRawData[childrenMapKey]['root-child'][valuesMapKey]['foo'], 42);
final RestorationBucket firstBucket = state.bucket;
// Replace with new root.
final Map<String, dynamic> secondRawData = <String, dynamic>{
childrenMapKey: <String, dynamic>{
'root-child': <String, dynamic>{
valuesMapKey: <String, dynamic>{
'foo': 22,
},
},
},
};
final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot);
firstRoot..decommission()..dispose();
await tester.pump();
expect(state.bucket, isNot(same(firstBucket)));
expect(state.bucket.read<int>('foo'), 22);
});
testWidgets('injects null when rootBucket is null', (WidgetTester tester) async {
final Completer<RestorationBucket> completer = Completer<RestorationBucket>();
binding.restorationManager.rootBucket = completer.future;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsNothing);
completer.complete(null);
await tester.pump(const Duration(milliseconds: 100));
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNull);
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pump();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNotNull);
});
testWidgets('can switch to null', (WidgetTester tester) async {
final RestorationBucket root = RestorationBucket.root(manager: binding.restorationManager, rawData: null);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(root);
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: RootRestorationScope(
restorationId: 'root-child',
child: BucketSpy(
child: Text('Hello'),
),
),
),
);
expect(binding.restorationManager.rootBucketAccessed, 1);
expect(find.text('Hello'), findsOneWidget);
final BucketSpyState state = tester.state(find.byType(BucketSpy));
expect(state.bucket, isNotNull);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(null);
root..decommission()..dispose();
await tester.pump();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
expect(state.bucket, isNull);
});
}
class TestAutomatedTestWidgetsFlutterBinding extends AutomatedTestWidgetsFlutterBinding {
MockRestorationManager _restorationManager;
@override
MockRestorationManager get restorationManager => _restorationManager;
@override
TestRestorationManager createRestorationManager() {
return null;
}
int _deferred = 0;
bool get firstFrameIsDeferred => _deferred > 0;
@override
void deferFirstFrame() {
_deferred++;
super.deferFirstFrame();
}
@override
void allowFirstFrame() {
_deferred--;
super.allowFirstFrame();
}
}
...@@ -58,6 +58,7 @@ export 'src/goldens.dart'; ...@@ -58,6 +58,7 @@ export 'src/goldens.dart';
export 'src/matchers.dart'; export 'src/matchers.dart';
export 'src/nonconst.dart'; export 'src/nonconst.dart';
export 'src/platform.dart'; export 'src/platform.dart';
export 'src/restoration.dart';
export 'src/stack_manipulation.dart'; export 'src/stack_manipulation.dart';
export 'src/test_async_utils.dart'; export 'src/test_async_utils.dart';
export 'src/test_compat.dart'; export 'src/test_compat.dart';
......
...@@ -23,6 +23,7 @@ import 'package:vector_math/vector_math_64.dart'; ...@@ -23,6 +23,7 @@ import 'package:vector_math/vector_math_64.dart';
import '_binding_io.dart' if (dart.library.html) '_binding_web.dart' as binding; import '_binding_io.dart' if (dart.library.html) '_binding_web.dart' as binding;
import 'goldens.dart'; import 'goldens.dart';
import 'platform.dart'; import 'platform.dart';
import 'restoration.dart';
import 'stack_manipulation.dart'; import 'stack_manipulation.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_exception_reporter.dart'; import 'test_exception_reporter.dart';
...@@ -187,6 +188,21 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -187,6 +188,21 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
TestWindow get window => _window; TestWindow get window => _window;
final TestWindow _window; final TestWindow _window;
@override
TestRestorationManager get restorationManager => _restorationManager;
TestRestorationManager _restorationManager;
/// Called by the test framework at the beginning of a widget test to
/// prepare the binding for the next test.
void reset() {
_restorationManager = createRestorationManager();
}
@override
TestRestorationManager createRestorationManager() {
return TestRestorationManager();
}
/// The value to set [debugPrint] to while tests are running. /// The value to set [debugPrint] to while tests are running.
/// ///
/// This can be used to redirect console output from the framework, or to /// This can be used to redirect console output from the framework, or to
......
// 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:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// The [RestorationManager] used for tests.
///
/// Unlike the real [RestorationManager], this one just keeps the restoration
/// data in memory and does not make it available to the engine.
class TestRestorationManager extends RestorationManager {
/// Creates a [TestRestorationManager].
TestRestorationManager() {
// Ensures that [rootBucket] always returns a synchronous future to avoid
// extra pumps in tests.
restoreFrom(TestRestorationData.empty);
}
@override
Future<RestorationBucket> get rootBucket {
_debugRootBucketAccessed = true;
return super.rootBucket;
}
/// The current restoration data from which the current state can be restored.
///
/// To restore the state to the one described by this data, pass the
/// [TestRestorationData] obtained from this getter back to [restoreFrom].
///
/// See also:
///
/// * [WidgetTester.getRestorationData], which makes this data available
/// in a widget test.
TestRestorationData get restorationData => _restorationData;
TestRestorationData _restorationData;
/// Whether the [rootBucket] has been obtained.
bool get debugRootBucketAccessed => _debugRootBucketAccessed;
bool _debugRootBucketAccessed = false;
/// Restores the state from the provided [TestRestorationData].
///
/// The restoration data obtained form [restorationData] can be passed into
/// this method to restore the state to what it was when the restoration data
/// was originally retrieved.
///
/// See also:
///
/// * [WidgetTester.restoreFrom], which exposes this method to a widget test.
void restoreFrom(TestRestorationData data) {
_restorationData = data;
handleRestorationUpdateFromEngine(enabled: true, data: data.binary);
}
@override
Future<void> sendToEngine(Uint8List encodedData) async {
_restorationData = TestRestorationData._(encodedData);
}
}
/// Restoration data that can be used to restore the state to the one described
/// by this data.
///
/// See also:
///
/// * [WidgetTester.getRestorationData], which retrieves the restoration data
/// from the widget under test.
/// * [WidgetTester.restoreFrom], which takes a [TestRestorationData] to
/// restore the state of the widget under test from the provided data.
class TestRestorationData {
const TestRestorationData._(this.binary);
/// Empty restoration data indicating that no data is available to restore
/// state from.
static const TestRestorationData empty = TestRestorationData._(null);
/// The serialized representation of the restoration data.
///
/// Should only be accessed by the test framework.
@protected
final Uint8List binary;
}
...@@ -23,6 +23,7 @@ import 'controller.dart'; ...@@ -23,6 +23,7 @@ import 'controller.dart';
import 'event_simulation.dart'; import 'event_simulation.dart';
import 'finders.dart'; import 'finders.dart';
import 'matchers.dart'; import 'matchers.dart';
import 'restoration.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_compat.dart'; import 'test_compat.dart';
import 'test_pointer.dart'; import 'test_pointer.dart';
...@@ -137,6 +138,7 @@ void testWidgets( ...@@ -137,6 +138,7 @@ void testWidgets(
test_package.addTearDown(binding.postTest); test_package.addTearDown(binding.postTest);
return binding.runTest( return binding.runTest(
() async { () async {
binding.reset();
debugResetSemanticsIdCounter(); debugResetSemanticsIdCounter();
tester.resetTestTextInput(); tester.resetTestTextInput();
Object memento; Object memento;
...@@ -678,6 +680,53 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -678,6 +680,53 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
}); });
} }
/// Simulates restoring the state of the widget tree after the application
/// is restarted.
///
/// The method grabs the current serialized restoration data from the
/// [RestorationManager], takes down the widget tree to destroy all in-memory
/// state, and then restores the widget tree from the serialized restoration
/// data.
Future<void> restartAndRestore() async {
assert(
binding.restorationManager.debugRootBucketAccessed,
'The current widget tree did not inject the root bucket of the RestorationManager and '
'therefore no restoration data has been collected to restore from. Did you forget to wrap '
'your widget tree in a RootRestorationScope?',
);
final Widget widget = (binding.renderViewElement as RenderObjectToWidgetElement<RenderObject>).widget.child;
final TestRestorationData restorationData = binding.restorationManager.restorationData;
runApp(Container(key: UniqueKey()));
await pump();
binding.restorationManager.restoreFrom(restorationData);
return pumpWidget(widget);
}
/// Retrieves the current restoration data from the [RestorationManager].
///
/// The returned [TestRestorationData] describes the current state of the
/// widget tree under test and can be provided to [restoreFrom] to restore
/// the widget tree to the state described by this data.
Future<TestRestorationData> getRestorationData() async {
assert(
binding.restorationManager.debugRootBucketAccessed,
'The current widget tree did not inject the root bucket of the RestorationManager and '
'therefore no restoration data has been collected. Did you forget to wrap your widget tree '
'in a RootRestorationScope?',
);
return binding.restorationManager.restorationData;
}
/// Restores the widget tree under test to the state described by the
/// provided [TestRestorationData].
///
/// The data provided to this method is usually obtained from
/// [getRestorationData].
Future<void> restoreFrom(TestRestorationData data) {
binding.restorationManager.restoreFrom(data);
return pump();
}
/// Runs a [callback] that performs real asynchronous work. /// Runs a [callback] that performs real asynchronous work.
/// ///
/// This is intended for callers who need to call asynchronous methods where /// This is intended for callers who need to call asynchronous methods where
......
// 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/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('restartAndRestore', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(
restorationId: 'restorable-widget',
),
),
);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
expect(find.text('Hello World 100'), findsOneWidget);
expect(state.doubleValue, 1.0);
state.setValues('Guten Morgen', 200, 33.4);
await tester.pump();
expect(find.text('Guten Morgen 200'), findsOneWidget);
expect(state.doubleValue, 33.4);
await tester.restartAndRestore();
expect(find.text('Guten Morgen 200'), findsOneWidget);
expect(find.text('Hello World 100'), findsNothing);
final _RestorableWidgetState restoredState = tester.state(find.byType(_RestorableWidget));
expect(restoredState, isNot(same(state)));
expect(restoredState.doubleValue, 1.0);
});
testWidgets('restore from previous restoration data', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
restorationId: 'root-child',
child: _RestorableWidget(
restorationId: 'restorable-widget',
),
),
);
final _RestorableWidgetState state = tester.state(find.byType(_RestorableWidget));
expect(find.text('Hello World 100'), findsOneWidget);
expect(state.doubleValue, 1.0);
state.setValues('Guten Morgen', 200, 33.4);
await tester.pump();
expect(find.text('Guten Morgen 200'), findsOneWidget);
expect(state.doubleValue, 33.4);
final TestRestorationData data = await tester.getRestorationData();
state.setValues('See you later!', 400, 123.5);
await tester.pump();
expect(find.text('See you later! 400'), findsOneWidget);
expect(state.doubleValue, 123.5);
await tester.restoreFrom(data);
expect(tester.state(find.byType(_RestorableWidget)), same(state));
expect(find.text('Guten Morgen 200'), findsOneWidget);
expect(state.doubleValue, 123.5);
});
}
class _RestorableWidget extends StatefulWidget {
const _RestorableWidget({Key key, this.restorationId}) : super(key: key);
final String restorationId;
@override
State<_RestorableWidget> createState() => _RestorableWidgetState();
}
class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMixin {
final RestorableString stringValue = RestorableString('Hello World');
final RestorableInt intValue = RestorableInt(100);
double doubleValue = 1.0; // Not restorable.
@override
void restoreState(RestorationBucket oldBucket) {
registerForRestoration(stringValue, 'string');
registerForRestoration(intValue, 'int');
}
void setValues(String s, int i, double d) {
setState(() {
stringValue.value = s;
intValue.value = i;
doubleValue = d;
});
}
@override
Widget build(BuildContext context) {
return Text('${stringValue.value} ${intValue.value}', textDirection: TextDirection.ltr);
}
@override
String get restorationId => widget.restorationId;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment