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

Remove decommission from RestorationBuckets (#63687)

parent 06c3de32
......@@ -104,7 +104,7 @@ class _RestorationScopeState extends State<RestorationScope> with RestorationMix
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket oldBucket) {
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
// Nothing to do.
// The bucket gets injected into the widget tree in the build method.
}
......@@ -648,7 +648,7 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
/// String get restorationId => widget.restorationId;
///
/// @override
/// void restoreState(RestorationBucket oldBucket) {
/// void restoreState(RestorationBucket oldBucket, bool initialRestore) {
/// // All restorable properties must be registered with the mixin. After
/// // registration, the counter either has its old value restored or is
/// // initialized to its default value.
......@@ -783,7 +783,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// [bucket].
@mustCallSuper
@protected
void restoreState(RestorationBucket oldBucket);
void restoreState(RestorationBucket oldBucket, bool initialRestore);
/// Called when [bucket] switches between null and non-null values.
///
......@@ -804,8 +804,8 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
@mustCallSuper
@protected
void didToggleBucket(RestorationBucket oldBucket) {
// When restore is pending, restoreState must be called instead.
assert(!restorePending);
// When a bucket is replaced, must `restoreState` is called instead.
assert(_bucket?.isReplacing != true);
}
// Maps properties to their listeners.
......@@ -900,8 +900,22 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// [restorationId] was caused by an updated widget.
@protected
void didUpdateRestorationId() {
if (_bucket?.restorationId != restorationId && !restorePending) {
_updateBucketIfNecessary();
// There's nothing to do if:
// - We don't have a parent to claim a bucket from.
// - Our current bucket already uses the provided restoration ID.
// - There's a restore pending, which means that 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,98 +937,105 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
/// While this is true, [bucket] will also still return the old bucket with
/// the old restoration data. It will update to the new bucket with the new
/// data just before [restoreState] is invoked.
bool get restorePending => _restorePending;
bool _restorePending = true;
bool get restorePending {
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;
bool get _debugDoingRestore => _debugPropertiesWaitingForReregistration != null;
bool _firstRestorePending = true;
RestorationBucket _currentParent;
@override
void didChangeDependencies() {
super.didChangeDependencies();
RestorationBucket oldBucket;
if (_restorePending) {
oldBucket = _bucket;
// Throw away the old bucket so [_updateBucketIfNecessary] will claim a
// new one with the new restoration data.
_bucket = null;
}
_updateBucketIfNecessary();
if (_restorePending) {
_restorePending = false;
assert(() {
_debugPropertiesWaitingForReregistration = _properties.keys.toList();
return true;
}());
restoreState(oldBucket);
assert(() {
if (_debugPropertiesWaitingForReregistration.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Previously registered RestorableProperties must be re-registered in "restoreState".',
),
ErrorDescription(
'The RestorableProperties with the following IDs were not re-registered to $this when '
'"restoreState" was called:',
),
..._debugPropertiesWaitingForReregistration.map((RestorableProperty<Object> property) => ErrorDescription(
' * ${property._restorationId}',
)),
]);
}
_debugPropertiesWaitingForReregistration = null;
return true;
}());
final RestorationBucket oldBucket = _bucket;
final bool needsRestore = restorePending;
_currentParent = RestorationScope.of(context);
final bool didReplaceBucket = _updateBucketIfNecessary(parent: _currentParent, restorePending: needsRestore);
if (needsRestore) {
_doRestore(oldBucket);
}
if (didReplaceBucket) {
assert(oldBucket != _bucket);
oldBucket?.dispose();
}
}
void _markNeedsRestore() {
_restorePending = true;
// [didChangeDependencies] will be called next because our bucket can only
// become invalid if our parent bucket ([RestorationScope.of]) is replaced
// with a new one.
void _doRestore(RestorationBucket oldBucket) {
assert(() {
_debugPropertiesWaitingForReregistration = _properties.keys.toList();
return true;
}());
restoreState(oldBucket, _firstRestorePending);
_firstRestorePending = false;
assert(() {
if (_debugPropertiesWaitingForReregistration.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Previously registered RestorableProperties must be re-registered in "restoreState".',
),
ErrorDescription(
'The RestorableProperties with the following IDs were not re-registered to $this when '
'"restoreState" was called:',
),
..._debugPropertiesWaitingForReregistration.map((RestorableProperty<Object> property) => ErrorDescription(
' * ${property._restorationId}',
)),
]);
}
_debugPropertiesWaitingForReregistration = null;
return true;
}());
}
void _updateBucketIfNecessary() {
if (restorationId == null) {
_setNewBucketIfNecessary(newBucket: null);
// Returns true if `bucket` has been replaced with a new bucket. It's the
// responsibility of the caller to dispose the old bucket when this returns true.
bool _updateBucketIfNecessary({
@required RestorationBucket parent,
@required bool restorePending,
}) {
if (restorationId == null || parent == null) {
final bool didReplace = _setNewBucketIfNecessary(newBucket: null, restorePending: restorePending);
assert(_bucket == null);
return;
return didReplace;
}
final RestorationBucket newParent = RestorationScope.of(context);
if (newParent == null) {
_setNewBucketIfNecessary(newBucket: null);
assert(_bucket == null);
return;
}
if (_bucket == null) {
assert(newParent != null);
assert(restorationId != null);
final RestorationBucket newBucket = newParent.claimChild(restorationId, debugOwner: this)
..addListener(_markNeedsRestore);
assert(restorationId != null);
assert(parent != null);
if (restorePending || _bucket == null) {
final RestorationBucket newBucket = parent.claimChild(restorationId, debugOwner: this);
assert(newBucket != null);
_setNewBucketIfNecessary(newBucket: newBucket);
final bool didReplace = _setNewBucketIfNecessary(newBucket: newBucket, restorePending: restorePending);
assert(_bucket == newBucket);
return;
return didReplace;
}
// We have an existing bucket, make sure it has the right parent and id.
assert(_bucket != null);
assert(newParent != null);
assert(restorationId != null);
assert(!restorePending);
_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) {
return;
return false;
}
assert(newBucket == null || _bucket == null);
final RestorationBucket oldBucket = _bucket;
_bucket = newBucket;
if (!restorePending) {
......@@ -1024,7 +1045,7 @@ mixin RestorationMixin<S extends StatefulWidget> on State<S> {
}
didToggleBucket(oldBucket);
}
oldBucket?.dispose();
return true;
}
void _updateProperty(RestorableProperty<Object> property) {
......
......@@ -402,11 +402,11 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
}
@override
void restoreState(RestorationBucket oldBucket) {
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(_persistedScrollOffset, 'offset');
assert(position != 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 {
Future<RestorationBucket> _rootBucket;
set rootBucket(Future<RestorationBucket> value) {
_rootBucket = value;
_isRestoring = true;
ServicesBinding.instance.addPostFrameCallback((Duration _) {
_isRestoring = false;
});
notifyListeners();
}
@override
bool get isReplacing => _isRestoring;
bool _isRestoring;
@override
Future<void> sendToEngine(Uint8List encodedData) {
......
......@@ -541,60 +541,12 @@ void main() {
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);
......@@ -605,11 +557,6 @@ void main() {
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() {
......
......@@ -126,26 +126,21 @@ void main() {
final RestorationBucket child = rootBucket.claimChild('child1', debugOwner: null);
expect(child.read<int>('another value'), 22);
bool rootDecommissioned = false;
bool childDecommissioned = false;
bool rootReplaced = false;
RestorationBucket newRoot;
rootBucket.addListener(() {
rootDecommissioned = true;
manager.addListener(() {
rootReplaced = 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(rootReplaced, isTrue);
expect(newRoot, isNot(same(rootBucket)));
child.dispose();
......@@ -234,6 +229,60 @@ void main() {
manager.flushData();
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', () {
......@@ -305,3 +354,9 @@ Map<dynamic, dynamic> _packageRestorationData({bool enabled = true, Map<dynamic,
'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
final _TestRestorableValue objectValue = _TestRestorableValue();
@override
void restoreState(RestorationBucket oldBucket) {
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(numValue, 'num');
registerForRestoration(doubleValue, 'double');
registerForRestoration(intValue, 'int');
......
......@@ -356,50 +356,6 @@ void main() {
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(
......@@ -711,7 +667,7 @@ class _TestRestorableWidgetState extends State<_TestRestorableWidget> with Resto
@override
void restoreState(RestorationBucket oldBucket) {
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
restoreStateLog.add(oldBucket);
registerForRestoration(property, 'foo');
if (_rerigisterAdditionalProperty && additionalProperty != null) {
......
......@@ -323,45 +323,6 @@ void main() {
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);
});
});
}
......
// 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() {
};
final RestorationBucket secondRoot = RestorationBucket.root(manager: binding.restorationManager, rawData: secondRawData);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(secondRoot);
firstRoot..decommission()..dispose();
await tester.pump();
firstRoot.dispose();
expect(state.bucket, isNot(same(firstBucket)));
expect(state.bucket.read<int>('foo'), 22);
......@@ -362,8 +362,8 @@ void main() {
expect(state.bucket, isNotNull);
binding.restorationManager.rootBucket = SynchronousFuture<RestorationBucket>(null);
root..decommission()..dispose();
await tester.pump();
root.dispose();
expect(binding.restorationManager.rootBucketAccessed, 2);
expect(find.text('Hello'), findsOneWidget);
......
......@@ -55,6 +55,14 @@ class TestRestorationManager extends RestorationManager {
handleRestorationUpdateFromEngine(enabled: true, data: data.binary);
}
/// Disabled state restoration.
///
/// To turn restoration back on call [restoreFrom].
void disableRestoration() {
_restorationData = null;
handleRestorationUpdateFromEngine(enabled: false);
}
@override
Future<void> sendToEngine(Uint8List encodedData) async {
_restorationData = TestRestorationData._(encodedData);
......
......@@ -88,7 +88,7 @@ class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMi
double doubleValue = 1.0; // Not restorable.
@override
void restoreState(RestorationBucket oldBucket) {
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(stringValue, 'string');
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