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

Restoration Framework (#60375)

* state restoration

* added example

* typos and analyzer

* whitespace

* more typos

* remove unnecessary import

* whitespace

* fix sample code

* tests for restorationmanager and restorationid

* ++

* typo

* tests for bucket, part1

* rename tests

* more tests

* finished tests for service layer

* remove wrong todo

* ++

* review comments

* tests for Unmanaged and regular scope

* RootRestorationScope tests

* typo

* whitespace

* testing framework

* tests for properties

* last set of tests

* analyzer

* typo

* dan review

* whitespace

* ++

* refactor finalizers

* ++

* ++

* dispose guard

* ++

* ++

* dan review

* add manager assert

* ++

* analyzer

* greg review

* fix typo

* Ian & John review

* ian review

* RestorationID -> String

* revert comment

* Make primitives non-nullable in prep for NNBD
parent 1fff1050
......@@ -33,6 +33,7 @@ export 'src/services/raw_keyboard_linux.dart';
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';
......
......@@ -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].
......
This diff is collapsed.
......@@ -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(),
);
}
This diff is collapsed.
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'editable_text.dart';
import 'restoration.dart';
/// A [RestorableProperty] that makes the wrapped value accessible to the owning
/// [State] object via the [value] getter and setter.
///
/// Whenever a new [value] is set, [didUpdateValue] is called. Subclasses should
/// call [notifyListeners] from this method if the new value changes what
/// [toPrimitives] returns.
///
/// See also:
///
/// * [RestorableProperty], which is the super class of this class.
/// * [RestorationMixin], to which a [RestorableValue] needs to be registered
/// in order to work.
/// * [RestorationManager], which provides an overview of how state restoration
/// works in Flutter.
abstract class RestorableValue<T> extends RestorableProperty<T> {
/// The current value stored in this property.
///
/// A representation of the current value is stored in the restoration data.
/// During state restoration, the property will restore the value to what it
/// was when the restoration data it is getting restored from was collected.
///
/// The [value] can only be accessed after the property has been registered
/// with a [RestorationMixin] by calling
/// [RestorationMixin.registerForRestoration].
T get value {
assert(isRegistered);
return _value;
}
T _value;
set value(T newValue) {
assert(isRegistered);
if (newValue != _value) {
final T oldValue = _value;
_value = newValue;
didUpdateValue(oldValue);
}
}
@mustCallSuper
@override
void initWithValue(T value) {
_value = value;
}
/// Called whenever a new value is assigned to [value].
///
/// The new value can be accessed via the regular [value] getter and the
/// previous value is provided as `oldValue`.
///
/// Subclasses should call [notifyListeners] from this method, if the new
/// value changes what [toPrimitives] returns.
@protected
void didUpdateValue(T oldValue);
}
// _RestorablePrimitiveValue and its subclasses do not allow null values in
// anticipation of NNBD (non-nullability by default).
//
// If necessary, we can in the future define a new subclass hierarchy that
// does allow null values for primitive types. Borrowing from lisp where
// functions that returned a bool ended in 'p', a suggested naming scheme for
// these new subclasses could be to add 'N' (for nullable) to the end of a
// class name (e.g. RestorableIntN, RestorableStringN, etc.) to distinguish them
// from their non-nullable friends.
class _RestorablePrimitiveValue<T> extends RestorableValue<T> {
_RestorablePrimitiveValue(this._defaultValue)
: assert(_defaultValue != null),
assert(debugIsSerializableForRestoration(_defaultValue)),
super();
final T _defaultValue;
@override
T createDefaultValue() => _defaultValue;
@override
set value(T value) {
assert(value != null);
super.value = value;
}
@override
void didUpdateValue(T oldValue) {
assert(debugIsSerializableForRestoration(value));
notifyListeners();
}
@override
T fromPrimitives(Object serialized) {
assert(serialized != null);
return serialized as T;
}
@override
Object toPrimitives() {
assert(value != null);
return value;
}
}
/// A [RestorableProperty] that knows how to store and restore a [num].
///
/// {@template flutter.widgets.restoration.primitivevalue}
/// The current [value] of this property is stored in the restoration data.
/// During state restoration the property is restored to the value it had when
/// the restoration data it is getting restored from was collected.
///
/// If no restoration data is available, [value] is initialized to the
/// `defaultValue` given in the constructor.
/// {@endtemplate}
///
/// Instead of using the more generic [RestorableNum] directly, consider using
/// one of the more specific subclasses (e.g. [RestorableDouble] to store a
/// [double] and [RestorableInt] to store an [int]).
class RestorableNum<T extends num> extends _RestorablePrimitiveValue<T> {
/// Creates a [RestorableNum].
///
/// {@template flutter.widgets.restoration.primitivevalue.constructor}
/// If no restoration data is available to restore the value in this property
/// from, the property will be initialized with the provided `defaultValue`.
/// {@endtemplate}
RestorableNum(T defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [double].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableDouble extends RestorableNum<double> {
/// Creates a [RestorableDouble].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableDouble(double defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore an [int].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableInt extends RestorableNum<int> {
/// Creates a [RestorableInt].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableInt(int defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [String].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableString extends _RestorablePrimitiveValue<String> {
/// Creates a [RestorableString].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableString(String defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A [RestorableProperty] that knows how to store and restore a [bool].
///
/// {@macro flutter.widgets.restoration.primitivevalue}
class RestorableBool extends _RestorablePrimitiveValue<bool> {
/// Creates a [RestorableBool].
///
/// {@macro flutter.widgets.restoration.primitivevalue.constructor}
RestorableBool(bool defaultValue) : assert(defaultValue != null), super(defaultValue);
}
/// A base class for creating a [RestorableProperty] that stores and restores a
/// [Listenable].
///
/// This class may be used to implement a [RestorableProperty] for a
/// [Listenable], whose information it needs to store in the restoration data
/// change whenever the [Listenable] notifies its listeners.
///
/// The [RestorationMixin] this property is registered with will call
/// [toPrimitives] whenever the wrapped [Listenable] notifies its listeners to
/// update the information that this property has stored in the restoration
/// data.
abstract class RestorableListenable<T extends Listenable> extends RestorableProperty<T> {
/// The [Listenable] stored in this property.
///
/// A representation of the current value of the [Listenable] is stored in the
/// restoration data. During state restoration, the [Listenable] returned by
/// this getter will be restored to the state it had when the restoration data
/// the property is getting restored from was collected.
///
/// The [value] can only be accessed after the property has been registered
/// with a [RestorationMixin] by calling
/// [RestorationMixin.registerForRestoration].
T get value {
assert(isRegistered);
return _value;
}
T _value;
@override
void initWithValue(T value) {
assert(value != null);
_value?.removeListener(notifyListeners);
_value = value;
_value.addListener(notifyListeners);
}
@override
void dispose() {
super.dispose();
_value?.removeListener(notifyListeners);
}
}
/// A [RestorableProperty] that knows how to store and restore a
/// [TextEditingController].
///
/// The [TextEditingController] is accessible via the [value] getter. During
/// state restoration, the property will restore [TextEditingController.text] to
/// the value it had when the restoration data it is getting restored from was
/// collected.
class RestorableTextEditingController extends RestorableListenable<TextEditingController> {
/// Creates a [RestorableTextEditingController].
///
/// This constructor treats a null `text` argument as if it were the empty
/// string.
factory RestorableTextEditingController({String text}) => RestorableTextEditingController.fromValue(
text == null ? TextEditingValue.empty : TextEditingValue(text: text),
);
/// Creates a [RestorableTextEditingController] from an initial
/// [TextEditingValue].
///
/// This constructor treats a null `value` argument as if it were
/// [TextEditingValue.empty].
RestorableTextEditingController.fromValue(TextEditingValue value) : _initialValue = value;
final TextEditingValue _initialValue;
@override
TextEditingController createDefaultValue() {
return TextEditingController.fromValue(_initialValue);
}
@override
TextEditingController fromPrimitives(Object data) {
return TextEditingController(text: data as String);
}
@override
Object toPrimitives() {
return value.text;
}
@override
void dispose() {
if (isRegistered) {
value.dispose();
}
super.dispose();
}
}
......@@ -82,6 +82,8 @@ export 'src/widgets/platform_view.dart';
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';
......
// 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';
This diff is collapsed.
// 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)
};
}
This diff is collapsed.
// 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();
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -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';
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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