Unverified Commit 39a46bed authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Remove decommission from RestorationBuckets (#63687)

parent 06c3de32
...@@ -55,13 +55,10 @@ typedef _BucketVisitor = void Function(RestorationBucket bucket); ...@@ -55,13 +55,10 @@ typedef _BucketVisitor = void Function(RestorationBucket bucket);
/// In addition to providing restoration data when the app is launched, /// 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 /// 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 /// 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 /// browser). When this happens, the [RestorationManager] notifies its listeners
/// and replaced with the hierarchy deserialized from the newly provided /// (added via [addListener]) that a new [rootBucket] is available. In response
/// restoration data. Buckets in the old hierarchy notify their listeners when /// to the notification, listeners must stop using the old bucket and restore
/// they get decommissioned. In response to the notification, listeners must /// their state from the information in the new [rootBucket].
/// 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 /// Same platforms restrict the size of the restoration data. Therefore, the
/// data stored in the buckets should be as small as possible while still /// data stored in the buckets should be as small as possible while still
...@@ -147,6 +144,17 @@ class RestorationManager extends ChangeNotifier { ...@@ -147,6 +144,17 @@ class RestorationManager extends ChangeNotifier {
Completer<RestorationBucket>? _pendingRootBucket; Completer<RestorationBucket>? _pendingRootBucket;
bool _rootBucketIsValid = false; bool _rootBucketIsValid = false;
/// Returns true for the frame after [rootBucket] has been replaced with a
/// new non-null bucket.
///
/// When true, entities should forget their current state and restore
/// their state according to the information in the new [rootBucket].
///
/// The [RestorationManager] informs its listeners (added via [addListener])
/// when this flag changes from false to true.
bool get isReplacing => _isReplacing;
bool _isReplacing = false;
Future<void> _getRootBucketFromEngine() async { Future<void> _getRootBucketFromEngine() async {
final Map<dynamic, dynamic>? config = await SystemChannels.restoration.invokeMethod<Map<dynamic, dynamic>>('get'); final Map<dynamic, dynamic>? config = await SystemChannels.restoration.invokeMethod<Map<dynamic, dynamic>>('get');
if (_pendingRootBucket == null) { if (_pendingRootBucket == null) {
...@@ -172,11 +180,9 @@ class RestorationManager extends ChangeNotifier { ...@@ -172,11 +180,9 @@ class RestorationManager extends ChangeNotifier {
/// The `enabled` parameter indicates whether the engine wants to receive /// The `enabled` parameter indicates whether the engine wants to receive
/// restoration data. When `enabled` is false, state restoration is turned /// restoration data. When `enabled` is false, state restoration is turned
/// off and the [rootBucket] is set to null. When `enabled` is true, the /// off and the [rootBucket] is set to null. When `enabled` is true, the
/// provided restoration `data` will be parsed into the [rootBucket]. If /// provided restoration `data` will be parsed into a new [rootBucket]. If
/// `data` is null, an empty [rootBucket] will be instantiated. /// `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 /// Subclasses in test frameworks may call this method at any time to inject
/// restoration data (obtained e.g. by overriding [sendToEngine]) into the /// restoration data (obtained e.g. by overriding [sendToEngine]) into the
/// [RestorationManager]. When the method is called before the [rootBucket] is /// [RestorationManager]. When the method is called before the [rootBucket] is
...@@ -185,9 +191,16 @@ class RestorationManager extends ChangeNotifier { ...@@ -185,9 +191,16 @@ class RestorationManager extends ChangeNotifier {
@protected @protected
void handleRestorationUpdateFromEngine({required bool enabled, required Uint8List? data}) { void handleRestorationUpdateFromEngine({required bool enabled, required Uint8List? data}) {
assert(enabled != null); assert(enabled != null);
assert(enabled || data == null);
final RestorationBucket? oldRoot = _rootBucket; _isReplacing = _rootBucketIsValid && enabled;
if (_isReplacing) {
SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
_isReplacing = false;
});
}
final RestorationBucket? oldRoot = _rootBucket;
_rootBucket = enabled _rootBucket = enabled
? RestorationBucket.root(manager: this, rawData: _decodeRestorationData(data)) ? RestorationBucket.root(manager: this, rawData: _decodeRestorationData(data))
: null; : null;
...@@ -198,11 +211,7 @@ class RestorationManager extends ChangeNotifier { ...@@ -198,11 +211,7 @@ class RestorationManager extends ChangeNotifier {
if (_rootBucket != oldRoot) { if (_rootBucket != oldRoot) {
notifyListeners(); notifyListeners();
} oldRoot?.dispose();
if (oldRoot != null) {
oldRoot
..decommission()
..dispose();
} }
} }
...@@ -404,27 +413,13 @@ class RestorationManager extends ChangeNotifier { ...@@ -404,27 +413,13 @@ class RestorationManager extends ChangeNotifier {
/// stored in the bucket. If the bucket is empty, it may initialize itself to /// stored in the bucket. If the bucket is empty, it may initialize itself to
/// default values. /// 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 /// 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 /// 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 /// 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 stored in the bucket from the app's restoration data and that
/// information will not be available again when the application is restored to /// information will not be available again when the application is restored to
/// this state in the future. /// this state in the future.
class RestorationBucket extends ChangeNotifier { class RestorationBucket {
/// Creates an empty [RestorationBucket] to be provided to [adoptChild] to add /// Creates an empty [RestorationBucket] to be provided to [adoptChild] to add
/// it to the bucket hierarchy. /// it to the bucket hierarchy.
/// ///
...@@ -529,6 +524,16 @@ class RestorationBucket extends ChangeNotifier { ...@@ -529,6 +524,16 @@ class RestorationBucket extends ChangeNotifier {
RestorationManager? _manager; RestorationManager? _manager;
RestorationBucket? _parent; RestorationBucket? _parent;
/// Returns true when entities processing this bucket should restore their
/// state from the information in the bucket (e.g. via [read] and
/// [claimChild]) instead of copying their current state information into the
/// bucket (e.g. via [write] and [adoptChild].
///
/// This flag is true for the frame after the [RestorationManager] has been
/// instructed to restore the application from newly provided restoration
/// data.
bool get isReplacing => _manager?.isReplacing ?? false;
/// The restoration ID under which the bucket is currently stored in the /// 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 of this bucket (or wants to be stored if it is currently
/// parent-less). /// parent-less).
...@@ -545,49 +550,6 @@ class RestorationBucket extends ChangeNotifier { ...@@ -545,49 +550,6 @@ class RestorationBucket extends ChangeNotifier {
// Maps a restoration ID to a value that is stored in this bucket. // 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>; 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. // Get and store values.
/// Returns the value of type `P` that is currently stored in the bucket under /// Returns the value of type `P` that is currently stored in the bucket under
...@@ -782,7 +744,6 @@ class RestorationBucket extends ChangeNotifier { ...@@ -782,7 +744,6 @@ class RestorationBucket extends ChangeNotifier {
bool _needsSerialization = false; bool _needsSerialization = false;
void _markNeedsSerialization() { void _markNeedsSerialization() {
assert(_manager != null || _decommissioned);
if (!_needsSerialization) { if (!_needsSerialization) {
_needsSerialization = true; _needsSerialization = true;
_manager?.scheduleSerializationFor(this); _manager?.scheduleSerializationFor(this);
...@@ -903,8 +864,8 @@ class RestorationBucket extends ChangeNotifier { ...@@ -903,8 +864,8 @@ class RestorationBucket extends ChangeNotifier {
// Bucket management // Bucket management
/// Changes the restoration ID under which the bucket is stored in its parent /// Changes the restoration ID under which the bucket is (or will be) stored
/// to `newRestorationId`. /// in its parent to `newRestorationId`.
/// ///
/// No-op if the bucket is already stored under the provided id. /// No-op if the bucket is already stored under the provided id.
/// ///
...@@ -915,13 +876,12 @@ class RestorationBucket extends ChangeNotifier { ...@@ -915,13 +876,12 @@ class RestorationBucket extends ChangeNotifier {
void rename(String newRestorationId) { void rename(String newRestorationId) {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
assert(newRestorationId != null); assert(newRestorationId != null);
assert(_parent != null);
if (newRestorationId == restorationId) { if (newRestorationId == restorationId) {
return; return;
} }
_parent!._removeChildData(this); _parent?._removeChildData(this);
_restorationId = newRestorationId; _restorationId = newRestorationId;
_parent!._addChildData(this); _parent?._addChildData(this);
} }
/// Deletes the bucket and all the data stored in it from the bucket /// Deletes the bucket and all the data stored in it from the bucket
...@@ -936,7 +896,6 @@ class RestorationBucket extends ChangeNotifier { ...@@ -936,7 +896,6 @@ class RestorationBucket extends ChangeNotifier {
/// as well. /// as well.
/// ///
/// This method must only be called by the object's owner. /// This method must only be called by the object's owner.
@override
void dispose() { void dispose() {
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
_visitChildren(_dropChild, concurrentModification: true); _visitChildren(_dropChild, concurrentModification: true);
...@@ -945,7 +904,6 @@ class RestorationBucket extends ChangeNotifier { ...@@ -945,7 +904,6 @@ class RestorationBucket extends ChangeNotifier {
_parent?._removeChildData(this); _parent?._removeChildData(this);
_parent = null; _parent = null;
_updateManager(null); _updateManager(null);
super.dispose();
_debugDisposed = true; _debugDisposed = true;
} }
......
...@@ -104,7 +104,7 @@ class _RestorationScopeState extends State<RestorationScope> with RestorationMix ...@@ -104,7 +104,7 @@ class _RestorationScopeState extends State<RestorationScope> with RestorationMix
String get restorationId => widget.restorationId; String get restorationId => widget.restorationId;
@override @override
void restoreState(RestorationBucket oldBucket) { void restoreState(RestorationBucket oldBucket, bool initialRestore) {
// Nothing to do. // Nothing to do.
// The bucket gets injected into the widget tree in the build method. // The bucket gets injected into the widget tree in the build method.
} }
...@@ -648,7 +648,7 @@ abstract class RestorableProperty<T> extends ChangeNotifier { ...@@ -648,7 +648,7 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
/// String get restorationId => widget.restorationId; /// String get restorationId => widget.restorationId;
/// ///
/// @override /// @override
/// void restoreState(RestorationBucket oldBucket) { /// void restoreState(RestorationBucket oldBucket, bool initialRestore) {
/// // All restorable properties must be registered with the mixin. After /// // All restorable properties must be registered with the mixin. After
/// // registration, the counter either has its old value restored or is /// // registration, the counter either has its old value restored or is
/// // initialized to its default value. /// // initialized to its default value.
...@@ -783,7 +783,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -783,7 +783,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// [bucket]. /// [bucket].
@mustCallSuper @mustCallSuper
@protected @protected
void restoreState(RestorationBucket oldBucket); void restoreState(RestorationBucket oldBucket, bool initialRestore);
/// Called when [bucket] switches between null and non-null values. /// Called when [bucket] switches between null and non-null values.
/// ///
...@@ -804,8 +804,8 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -804,8 +804,8 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
@mustCallSuper @mustCallSuper
@protected @protected
void didToggleBucket(RestorationBucket oldBucket) { void didToggleBucket(RestorationBucket oldBucket) {
// When restore is pending, restoreState must be called instead. // When a bucket is replaced, must `restoreState` is called instead.
assert(!restorePending); assert(_bucket?.isReplacing != true);
} }
// Maps properties to their listeners. // Maps properties to their listeners.
...@@ -900,8 +900,22 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -900,8 +900,22 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// [restorationId] was caused by an updated widget. /// [restorationId] was caused by an updated widget.
@protected @protected
void didUpdateRestorationId() { void didUpdateRestorationId() {
if (_bucket?.restorationId != restorationId && !restorePending) { // There's nothing to do if:
_updateBucketIfNecessary(); // - We don't have a parent to claim a bucket from.
// - Our current bucket already uses the provided restoration ID.
// - There's a restore pending, which means that didUpdateDependencies
// will be called and we handle the rename there.
if (_currentParent == null || _bucket?.restorationId == restorationId || restorePending) {
return;
}
final RestorationBucket oldBucket = _bucket;
assert(!restorePending);
final bool didReplaceBucket = _updateBucketIfNecessary(parent: _currentParent, restorePending: false);
if (didReplaceBucket) {
assert(oldBucket != _bucket);
assert(_bucket == null || oldBucket == null);
oldBucket?.dispose();
} }
} }
...@@ -923,32 +937,50 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -923,32 +937,50 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// While this is true, [bucket] will also still return the old bucket with /// While this is true, [bucket] will also still return the old bucket with
/// the old restoration data. It will update to the new bucket with the new /// the old restoration data. It will update to the new bucket with the new
/// data just before [restoreState] is invoked. /// data just before [restoreState] is invoked.
bool get restorePending => _restorePending; bool get restorePending {
bool _restorePending = true; if (_firstRestorePending) {
return true;
}
if (restorationId == null) {
return false;
}
final RestorationBucket potentialNewParent = RestorationScope.of(context);
return potentialNewParent != _currentParent && potentialNewParent?.isReplacing == true;
}
List<RestorableProperty<Object>> _debugPropertiesWaitingForReregistration; List<RestorableProperty<Object>> _debugPropertiesWaitingForReregistration;
bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null; bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null;
bool _firstRestorePending = true;
RestorationBucket _currentParent;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
RestorationBucket oldBucket;
if (_restorePending) { final RestorationBucket oldBucket = _bucket;
oldBucket = _bucket; final bool needsRestore = restorePending;
// Throw away the old bucket so [_updateBucketIfNecessary] will claim a _currentParent = RestorationScope.of(context);
// new one with the new restoration data.
_bucket = null; final bool didReplaceBucket = _updateBucketIfNecessary(parent: _currentParent, restorePending: needsRestore);
if (needsRestore) {
_doRestore(oldBucket);
}
if (didReplaceBucket) {
assert(oldBucket != _bucket);
oldBucket?.dispose();
}
} }
_updateBucketIfNecessary();
if (_restorePending) {
_restorePending = false;
void _doRestore(RestorationBucket oldBucket) {
assert(() { assert(() {
_debugPropertiesWaitingForReregistration = _properties.keys.toList(); _debugPropertiesWaitingForReregistration = _properties.keys.toList();
return true; return true;
}()); }());
restoreState(oldBucket); restoreState(oldBucket, _firstRestorePending);
_firstRestorePending = false;
assert(() { assert(() {
if (_debugPropertiesWaitingForReregistration.isNotEmpty) { if (_debugPropertiesWaitingForReregistration.isNotEmpty) {
...@@ -968,53 +1000,42 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -968,53 +1000,42 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
_debugPropertiesWaitingForReregistration = null; _debugPropertiesWaitingForReregistration = null;
return true; return true;
}()); }());
oldBucket?.dispose();
}
} }
void _markNeedsRestore() { // Returns true if `bucket` has been replaced with a new bucket. It's the
_restorePending = true; // responsibility of the caller to dispose the old bucket when this returns true.
// [didChangeDependencies] will be called next because our bucket can only bool _updateBucketIfNecessary({
// become invalid if our parent bucket ([RestorationScope.of]) is replaced @required RestorationBucket parent,
// with a new one. @required bool restorePending,
} }) {
if (restorationId == null || parent == null) {
void _updateBucketIfNecessary() { final bool didReplace = _setNewBucketIfNecessary(newBucket: null, restorePending: restorePending);
if (restorationId == null) {
_setNewBucketIfNecessary(newBucket: null);
assert(_bucket == null);
return;
}
final RestorationBucket newParent = RestorationScope.of(context);
if (newParent == null) {
_setNewBucketIfNecessary(newBucket: null);
assert(_bucket == null); assert(_bucket == null);
return; return didReplace;
} }
if (_bucket == null) {
assert(newParent != null);
assert(restorationId != null); assert(restorationId != null);
final RestorationBucket newBucket = newParent.claimChild(restorationId, debugOwner: this) assert(parent != null);
..addListener(_markNeedsRestore); if (restorePending || _bucket == null) {
final RestorationBucket newBucket = parent.claimChild(restorationId, debugOwner: this);
assert(newBucket != null); assert(newBucket != null);
_setNewBucketIfNecessary(newBucket: newBucket); final bool didReplace = _setNewBucketIfNecessary(newBucket: newBucket, restorePending: restorePending);
assert(_bucket == newBucket); assert(_bucket == newBucket);
return; return didReplace;
} }
// We have an existing bucket, make sure it has the right parent and id. // We have an existing bucket, make sure it has the right parent and id.
assert(_bucket != null); assert(_bucket != null);
assert(newParent != null); assert(!restorePending);
assert(restorationId != null);
_bucket.rename(restorationId); _bucket.rename(restorationId);
newParent.adoptChild(_bucket); parent.adoptChild(_bucket);
return false;
} }
void _setNewBucketIfNecessary({@required RestorationBucket newBucket}) { // Returns true if `bucket` has been replaced with a new bucket. It's the
// responsibility of the caller to dispose the old bucket when this returns true.
bool _setNewBucketIfNecessary({@required RestorationBucket newBucket, @required bool restorePending}) {
if (newBucket == _bucket) { if (newBucket == _bucket) {
return; return false;
} }
assert(newBucket == null || _bucket == null);
final RestorationBucket oldBucket = _bucket; final RestorationBucket oldBucket = _bucket;
_bucket = newBucket; _bucket = newBucket;
if (!restorePending) { if (!restorePending) {
...@@ -1024,7 +1045,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> { ...@@ -1024,7 +1045,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
} }
didToggleBucket(oldBucket); didToggleBucket(oldBucket);
} }
oldBucket?.dispose(); return true;
} }
void _updateProperty(RestorableProperty<Object> property) { void _updateProperty(RestorableProperty<Object> property) {
......
...@@ -402,11 +402,11 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -402,11 +402,11 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
} }
@override @override
void restoreState(RestorationBucket oldBucket) { void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(_persistedScrollOffset, 'offset'); registerForRestoration(_persistedScrollOffset, 'offset');
assert(position != null); assert(position != null);
if (_persistedScrollOffset.value != null) { if (_persistedScrollOffset.value != null) {
position.restoreOffset(_persistedScrollOffset.value, initialRestore: oldBucket == null); position.restoreOffset(_persistedScrollOffset.value, initialRestore: initialRestore);
} }
} }
......
...@@ -50,9 +50,16 @@ class MockRestorationManager extends TestRestorationManager { ...@@ -50,9 +50,16 @@ class MockRestorationManager extends TestRestorationManager {
Future<RestorationBucket> _rootBucket; Future<RestorationBucket> _rootBucket;
set rootBucket(Future<RestorationBucket> value) { set rootBucket(Future<RestorationBucket> value) {
_rootBucket = value; _rootBucket = value;
_isRestoring = true;
ServicesBinding.instance.addPostFrameCallback((Duration _) {
_isRestoring = false;
});
notifyListeners(); notifyListeners();
} }
@override
bool get isReplacing => _isRestoring;
bool _isRestoring;
@override @override
Future<void> sendToEngine(Uint8List encodedData) { Future<void> sendToEngine(Uint8List encodedData) {
......
...@@ -541,60 +541,12 @@ void main() { ...@@ -541,60 +541,12 @@ void main() {
expect(rawData[childrenMapKey]['child1'][childrenMapKey]['child2'][valuesMapKey]['hello'], 'world'); 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', () { test('throws when used after dispose', () {
final RestorationBucket bucket = RestorationBucket.empty(restorationId: 'foo', debugOwner: null); final RestorationBucket bucket = RestorationBucket.empty(restorationId: 'foo', debugOwner: null);
bucket.dispose(); bucket.dispose();
expect(() => bucket.debugOwner, throwsFlutterError); expect(() => bucket.debugOwner, throwsFlutterError);
expect(() => bucket.restorationId, throwsFlutterError); expect(() => bucket.restorationId, throwsFlutterError);
expect(() => bucket.decommission(), throwsFlutterError);
expect(() => bucket.read<int>('foo'), throwsFlutterError); expect(() => bucket.read<int>('foo'), throwsFlutterError);
expect(() => bucket.write('foo', 10), throwsFlutterError); expect(() => bucket.write('foo', 10), throwsFlutterError);
expect(() => bucket.remove<int>('foo'), throwsFlutterError); expect(() => bucket.remove<int>('foo'), throwsFlutterError);
...@@ -605,11 +557,6 @@ void main() { ...@@ -605,11 +557,6 @@ void main() {
expect(() => bucket.rename('bar'), throwsFlutterError); expect(() => bucket.rename('bar'), throwsFlutterError);
expect(() => bucket.dispose(), 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() { Map<String, dynamic> _createRawDataSet() {
......
...@@ -126,26 +126,21 @@ void main() { ...@@ -126,26 +126,21 @@ void main() {
final RestorationBucket child = rootBucket.claimChild('child1', debugOwner: null); final RestorationBucket child = rootBucket.claimChild('child1', debugOwner: null);
expect(child.read<int>('another value'), 22); expect(child.read<int>('another value'), 22);
bool rootDecommissioned = false; bool rootReplaced = false;
bool childDecommissioned = false;
RestorationBucket newRoot; RestorationBucket newRoot;
rootBucket.addListener(() { manager.addListener(() {
rootDecommissioned = true; rootReplaced = true;
manager.rootBucket.then((RestorationBucket bucket) { manager.rootBucket.then((RestorationBucket bucket) {
newRoot = bucket; newRoot = bucket;
}); });
// The new bucket is available synchronously. // The new bucket is available synchronously.
expect(newRoot, isNotNull); expect(newRoot, isNotNull);
}); });
child.addListener(() {
childDecommissioned = true;
});
// Send new Data. // Send new Data.
await _pushDataFromEngine(_createEncodedRestorationData2()); await _pushDataFromEngine(_createEncodedRestorationData2());
expect(rootDecommissioned, isTrue); expect(rootReplaced, isTrue);
expect(childDecommissioned, isTrue);
expect(newRoot, isNot(same(rootBucket))); expect(newRoot, isNot(same(rootBucket)));
child.dispose(); child.dispose();
...@@ -234,6 +229,60 @@ void main() { ...@@ -234,6 +229,60 @@ void main() {
manager.flushData(); manager.flushData();
expect(callsToEngine, hasLength(1)); expect(callsToEngine, hasLength(1));
}); });
testWidgets('isReplacing', (WidgetTester tester) async {
final Completer<Map<dynamic, dynamic>> result = Completer<Map<dynamic, dynamic>>();
SystemChannels.restoration.setMockMethodCallHandler((MethodCall call) {
return result.future;
});
final TestRestorationManager manager = TestRestorationManager();
expect(manager.isReplacing, isFalse);
RestorationBucket rootBucket;
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket = bucket;
});
result.complete(_createEncodedRestorationData1());
await tester.idle();
expect(rootBucket, isNotNull);
expect(rootBucket.isReplacing, isFalse);
expect(manager.isReplacing, isFalse);
tester.binding.scheduleFrame();
await tester.pump();
expect(manager.isReplacing, isFalse);
expect(rootBucket.isReplacing, isFalse);
manager.receiveDataFromEngine(enabled: true, data: null);
RestorationBucket rootBucket2;
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket2 = bucket;
});
expect(rootBucket2, isNotNull);
expect(rootBucket2, isNot(same(rootBucket)));
expect(manager.isReplacing, isTrue);
expect(rootBucket2.isReplacing, isTrue);
await tester.idle();
expect(manager.isReplacing, isTrue);
expect(rootBucket2.isReplacing, isTrue);
tester.binding.scheduleFrame();
await tester.pump();
expect(manager.isReplacing, isFalse);
expect(rootBucket2.isReplacing, isFalse);
manager.receiveDataFromEngine(enabled: false, data: null);
RestorationBucket rootBucket3;
manager.rootBucket.then((RestorationBucket bucket) {
rootBucket3 = bucket;
});
expect(rootBucket3, isNull);
expect(manager.isReplacing, isFalse);
await tester.idle();
expect(manager.isReplacing, isFalse);
tester.binding.scheduleFrame();
await tester.pump();
expect(manager.isReplacing, isFalse);
});
}); });
test('debugIsSerializableForRestoration', () { test('debugIsSerializableForRestoration', () {
...@@ -305,3 +354,9 @@ Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic, ...@@ -305,3 +354,9 @@ Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic,
'data': encoded == null ? null : encoded.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes) 'data': encoded == null ? null : encoded.buffer.asUint8List(encoded.offsetInBytes, encoded.lengthInBytes)
}; };
} }
class TestRestorationManager extends RestorationManager {
void receiveDataFromEngine({@required bool enabled, @required Uint8List data}) {
handleRestorationUpdateFromEngine(enabled: enabled, data: data);
}
}
...@@ -353,7 +353,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi ...@@ -353,7 +353,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi
final _TestRestorableValue objectValue = _TestRestorableValue(); final _TestRestorableValue objectValue = _TestRestorableValue();
@override @override
void restoreState(RestorationBucket oldBucket) { void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(numValue, 'num'); registerForRestoration(numValue, 'num');
registerForRestoration(doubleValue, 'double'); registerForRestoration(doubleValue, 'double');
registerForRestoration(intValue, 'int'); registerForRestoration(intValue, 'int');
......
...@@ -356,50 +356,6 @@ void main() { ...@@ -356,50 +356,6 @@ void main() {
expect(rawData[childrenMapKey].containsKey('moving-child'), isTrue); 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 { testWidgets('restartAndRestore', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const RootRestorationScope( const RootRestorationScope(
...@@ -711,7 +667,7 @@ class _TestRestorableWidgetState extends State<_TestRestorableWidget> with Resto ...@@ -711,7 +667,7 @@ class _TestRestorableWidgetState extends State<_TestRestorableWidget> with Resto
@override @override
void restoreState(RestorationBucket oldBucket) { void restoreState(RestorationBucket oldBucket, bool initialRestore) {
restoreStateLog.add(oldBucket); restoreStateLog.add(oldBucket);
registerForRestoration(property, 'foo'); registerForRestoration(property, 'foo');
if (_rerigisterAdditionalProperty && additionalProperty != null) { if (_rerigisterAdditionalProperty && additionalProperty != null) {
......
...@@ -323,45 +323,6 @@ void main() { ...@@ -323,45 +323,6 @@ void main() {
expect(rawData[childrenMapKey]['fixed'], isEmpty); expect(rawData[childrenMapKey]['fixed'], isEmpty);
expect(rawData[childrenMapKey].containsKey('moving-child'), isTrue); 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);
});
}); });
} }
......
// 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('widget moves scopes during restore', (WidgetTester tester) async {
await tester.pumpWidget(RootRestorationScope(
restorationId: 'root',
child: Directionality(
textDirection: TextDirection.ltr,
child: TestWidgetWithCounterChild(),
),
));
expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild, true);
expect(find.text('Counter: 0'), findsOneWidget);
await tester.tap(find.text('Counter: 0'));
await tester.pump();
expect(find.text('Counter: 1'), findsOneWidget);
final TestRestorationData dataWithChild = await tester.getRestorationData();
tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild = false;
await tester.pump();
expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).restoreChild, false);
await tester.tap(find.text('Counter: 1'));
await tester.pump();
expect(find.text('Counter: 2'), findsOneWidget);
final TestRestorationData dataWithoutChild = await tester.getRestorationData();
// Child moves from outside to inside scope.
await tester.restoreFrom(dataWithChild);
expect(find.text('Counter: 1'), findsOneWidget);
await tester.tap(find.text('Counter: 1'));
await tester.pump();
expect(find.text('Counter: 2'), findsOneWidget);
// Child stays inside scope.
await tester.restoreFrom(dataWithChild);
expect(find.text('Counter: 1'), findsOneWidget);
await tester.tap(find.text('Counter: 1'));
await tester.tap(find.text('Counter: 1'));
await tester.tap(find.text('Counter: 1'));
await tester.tap(find.text('Counter: 1'));
await tester.tap(find.text('Counter: 1'));
await tester.pump();
expect(find.text('Counter: 6'), findsOneWidget);
// Child moves from inside to outside scope.
await tester.restoreFrom(dataWithoutChild);
expect(find.text('Counter: 6'), findsOneWidget);
await tester.tap(find.text('Counter: 6'));
await tester.pump();
expect(find.text('Counter: 7'), findsOneWidget);
// Child stays outside scope.
await tester.restoreFrom(dataWithoutChild);
expect(find.text('Counter: 7'), findsOneWidget);
expect(tester.state<TestWidgetWithCounterChildState>(find.byType(TestWidgetWithCounterChild)).toggleCount, 0);
});
testWidgets('restoration is turned on later', (WidgetTester tester) async {
tester.binding.restorationManager.disableRestoration();
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: Directionality(
textDirection: TextDirection.ltr,
child: TestWidget(
restorationId: 'foo',
),
),
));
final TestWidgetState state = tester.state<TestWidgetState>(find.byType(TestWidget));
expect(find.text('hello'), findsOneWidget);
expect(state.buckets.single, isNull);
expect(state.flags.single, isTrue);
expect(state.bucket, isNull);
state.buckets.clear();
state.flags.clear();
await tester.restoreFrom(TestRestorationData.empty);
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root-child',
child: Directionality(
textDirection: TextDirection.ltr,
child: TestWidget(
restorationId: 'foo',
),
),
));
expect(find.text('hello'), findsOneWidget);
expect(state.buckets.single, isNull);
expect(state.flags.single, isFalse);
expect(state.bucket, isNotNull);
expect(state.toggleCount, 0);
});
}
class TestWidgetWithCounterChild extends StatefulWidget {
@override
State<TestWidgetWithCounterChild> createState() => TestWidgetWithCounterChildState();
}
class TestWidgetWithCounterChildState extends State<TestWidgetWithCounterChild> with RestorationMixin {
final RestorableBool childRestorationEnabled = RestorableBool(true);
int toggleCount = 0;
@override
void didToggleBucket(RestorationBucket oldBucket) {
super.didToggleBucket(oldBucket);
toggleCount++;
}
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(childRestorationEnabled, 'childRestorationEnabled');
}
bool get restoreChild => childRestorationEnabled.value;
set restoreChild(bool value) {
if (value == childRestorationEnabled.value) {
return;
}
setState(() {
childRestorationEnabled.value = value;
});
}
@override
void dispose() {
childRestorationEnabled.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Counter(
restorationId: restoreChild ? 'counter' : null,
);
}
@override
String get restorationId => 'foo';
}
class Counter extends StatefulWidget {
const Counter({this.restorationId});
final String restorationId;
@override
State<Counter> createState() => CounterState();
}
class CounterState extends State<Counter> with RestorationMixin {
final RestorableInt count = RestorableInt(0);
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(count, 'counter');
}
@override
void dispose() {
count.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return OutlinedButton(
onPressed: () {
setState(() {
count.value++;
});
},
child: Text(
'Counter: ${count.value}',
),
);
}
}
class TestWidget extends StatefulWidget {
const TestWidget({@required this.restorationId});
final String restorationId;
@override
State<TestWidget> createState() => TestWidgetState();
}
class TestWidgetState extends State<TestWidget> with RestorationMixin {
List<RestorationBucket> buckets = <RestorationBucket>[];
List<bool> flags = <bool>[];
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
buckets.add(oldBucket);
flags.add(initialRestore);
}
int toggleCount = 0;
@override
void didToggleBucket(RestorationBucket oldBucket) {
super.didToggleBucket(oldBucket);
toggleCount++;
}
@override
String get restorationId => widget.restorationId;
@override
Widget build(BuildContext context) {
return const Text('hello');
}
}
...@@ -296,8 +296,8 @@ void main() { ...@@ -296,8 +296,8 @@ void main() {
}; };
final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData); final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot); binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot);
firstRoot..decommission()..dispose();
await tester.pump(); await tester.pump();
firstRoot.dispose();
expect(state.bucket, isNot(same(firstBucket))); expect(state.bucket, isNot(same(firstBucket)));
expect(state.bucket.read<int>('foo'), 22); expect(state.bucket.read<int>('foo'), 22);
...@@ -362,8 +362,8 @@ void main() { ...@@ -362,8 +362,8 @@ void main() {
expect(state.bucket, isNotNull); expect(state.bucket, isNotNull);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(null); binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(null);
root..decommission()..dispose();
await tester.pump(); await tester.pump();
root.dispose();
expect(binding.restorationManager.rootBucketAccessed, 2); expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget); expect(find.text('Hello'), findsOneWidget);
......
...@@ -55,6 +55,14 @@ class TestRestorationManager extends RestorationManager { ...@@ -55,6 +55,14 @@ class TestRestorationManager extends RestorationManager {
handleRestorationUpdateFromEngine(enabled: true, data: data.binary); handleRestorationUpdateFromEngine(enabled: true, data: data.binary);
} }
/// Disabled state restoration.
///
/// To turn restoration back on call [restoreFrom].
void disableRestoration() {
_restorationData = null;
handleRestorationUpdateFromEngine(enabled: false);
}
@override @override
Future<void> sendToEngine(Uint8List encodedData) async { Future<void> sendToEngine(Uint8List encodedData) async {
_restorationData = TestRestorationData._(encodedData); _restorationData = TestRestorationData._(encodedData);
......
...@@ -88,7 +88,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi ...@@ -88,7 +88,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi
double doubleValue = 1.0; // Not restorable. double doubleValue = 1.0; // Not restorable.
@override @override
void restoreState(RestorationBucket oldBucket) { void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(stringValue, 'string'); registerForRestoration(stringValue, 'string');
registerForRestoration(intValue, 'int'); registerForRestoration(intValue, 'int');
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment