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
This commit is contained in:
Michael Goderbauer 2020-07-28 10:16:36 -07:00 committed by GitHub
parent 1fff10500f
commit 175e5c9aca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 5599 additions and 0 deletions

View File

@ -33,6 +33,7 @@ export 'src/services/raw_keyboard_linux.dart';
export 'src/services/raw_keyboard_macos.dart';
export 'src/services/raw_keyboard_web.dart';
export 'src/services/raw_keyboard_windows.dart';
export 'src/services/restoration.dart';
export 'src/services/system_channels.dart';
export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart';

View File

@ -13,6 +13,7 @@ import 'package:flutter/scheduler.dart';
import 'asset_bundle.dart';
import 'binary_messenger.dart';
import 'restoration.dart';
import 'system_channels.dart';
/// Listens for platform messages and directs them to the [defaultBinaryMessenger].
@ -27,6 +28,7 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
super.initInstances();
_instance = this;
_defaultBinaryMessenger = createBinaryMessenger();
_restorationManager = createRestorationManager();
window.onPlatformMessage = defaultBinaryMessenger.handlePlatformMessage;
initLicenses();
SystemChannels.system.setMessageHandler(handleSystemMessage);
@ -204,6 +206,27 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
}
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].

View File

@ -0,0 +1,963 @@
// 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;
}

View File

@ -286,4 +286,35 @@ class SystemChannels {
'flutter/mousecursor',
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(),
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
// 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();
}
}

View File

@ -82,6 +82,8 @@ export 'src/widgets/platform_view.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.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/safe_area.dart';
export 'src/widgets/scroll_activity.dart';

View File

@ -0,0 +1,67 @@
// 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';

View File

@ -0,0 +1,629 @@
// 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,
},
},
},
};
}

View File

@ -0,0 +1,270 @@
// 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)
};
}

View File

@ -0,0 +1,377 @@
// 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';
}

View File

@ -0,0 +1,34 @@
// 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();
}
}

View File

@ -0,0 +1,833 @@
// 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;
}
}

View File

@ -0,0 +1,387 @@
// 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,
}
},
},
};
}

View File

@ -0,0 +1,400 @@
// 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();
}
}

View File

@ -58,6 +58,7 @@ export 'src/goldens.dart';
export 'src/matchers.dart';
export 'src/nonconst.dart';
export 'src/platform.dart';
export 'src/restoration.dart';
export 'src/stack_manipulation.dart';
export 'src/test_async_utils.dart';
export 'src/test_compat.dart';

View File

@ -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 'goldens.dart';
import 'platform.dart';
import 'restoration.dart';
import 'stack_manipulation.dart';
import 'test_async_utils.dart';
import 'test_exception_reporter.dart';
@ -187,6 +188,21 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
TestWindow get window => _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.
///
/// This can be used to redirect console output from the framework, or to

View File

@ -0,0 +1,85 @@
// 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;
}

View File

@ -23,6 +23,7 @@ import 'controller.dart';
import 'event_simulation.dart';
import 'finders.dart';
import 'matchers.dart';
import 'restoration.dart';
import 'test_async_utils.dart';
import 'test_compat.dart';
import 'test_pointer.dart';
@ -137,6 +138,7 @@ void testWidgets(
test_package.addTearDown(binding.postTest);
return binding.runTest(
() async {
binding.reset();
debugResetSemanticsIdCounter();
tester.resetTestTextInput();
Object memento;
@ -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.
///
/// This is intended for callers who need to call asynchronous methods where

View File

@ -0,0 +1,111 @@
// 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;
}