Unverified Commit 25de9419 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Make Scrollables restorable (#63131)

parent a84e3902
......@@ -250,7 +250,7 @@ class RestorationManager extends ChangeNotifier {
}
bool _debugDoingUpdate = false;
bool _postFrameScheduled = false;
bool _serializationScheduled = false;
final Set<RestorationBucket> _bucketsNeedingSerialization = <RestorationBucket>{};
......@@ -270,8 +270,8 @@ class RestorationManager extends ChangeNotifier {
assert(bucket._manager == this);
assert(!_debugDoingUpdate);
_bucketsNeedingSerialization.add(bucket);
if (!_postFrameScheduled) {
_postFrameScheduled = true;
if (!_serializationScheduled) {
_serializationScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback((Duration _) => _doSerialization());
}
}
......@@ -295,11 +295,14 @@ class RestorationManager extends ChangeNotifier {
}
void _doSerialization() {
if (!_serializationScheduled) {
return;
}
assert(() {
_debugDoingUpdate = true;
return true;
}());
_postFrameScheduled = false;
_serializationScheduled = false;
for (final RestorationBucket bucket in _bucketsNeedingSerialization) {
bucket.finalize();
......@@ -312,6 +315,32 @@ class RestorationManager extends ChangeNotifier {
return true;
}());
}
/// Called to manually flush the restoration data to the engine.
///
/// A change in restoration data is usually accompanied by scheduling a frame
/// (because the restoration data is modified inside a [State.setState] call,
/// because it is usually something that affects the interface). Restoration
/// data is automatically flushed to the engine at the end of a frame. As a
/// result, it is uncommon to need to call this method directly. However, if
/// restoration data is changed without triggering a frame, this method must
/// be called to ensure that the updated restoration data is sent to the
/// engine in a timely manner. An example of such a use case is the
/// [Scrollable], where the final scroll offset after a scroll activity
/// finishes is determined between frames without scheduling a new frame.
///
/// Calling this method is a no-op if a frame is already scheduled. In that
/// case, the restoration data will be flushed to the engine at the end of
/// that frame. If this method is called and no frame is scheduled, the
/// current restoration data is directly sent to the engine.
void flushData() {
assert(!_debugDoingUpdate);
if (SchedulerBinding.instance!.hasScheduledFrame) {
return;
}
_doSerialization();
assert(!_serializationScheduled);
}
}
/// A [RestorationBucket] holds pieces of the restoration data that a part of
......
......@@ -434,12 +434,14 @@ class _FixedExtentScrollable extends Scrollable {
ScrollPhysics physics,
@required this.itemExtent,
@required ViewportBuilder viewportBuilder,
String restorationId,
}) : super (
key: key,
axisDirection: axisDirection,
controller: controller,
physics: physics,
viewportBuilder: viewportBuilder,
restorationId: restorationId,
);
final double itemExtent;
......@@ -585,6 +587,7 @@ class ListWheelScrollView extends StatefulWidget {
this.onSelectedItemChanged,
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
@required List<Widget> children,
}) : assert(children != null),
assert(diameterRatio != null),
......@@ -625,6 +628,7 @@ class ListWheelScrollView extends StatefulWidget {
this.onSelectedItemChanged,
this.renderChildrenOutsideViewport = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
@required this.childDelegate,
}) : assert(childDelegate != null),
assert(diameterRatio != null),
......@@ -713,6 +717,9 @@ class ListWheelScrollView extends StatefulWidget {
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String restorationId;
@override
_ListWheelScrollViewState createState() => _ListWheelScrollViewState();
}
......@@ -765,6 +772,7 @@ class _ListWheelScrollViewState extends State<ListWheelScrollView> {
controller: scrollController,
physics: widget.physics,
itemExtent: widget.itemExtent,
restorationId: widget.restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return ListWheelViewport(
diameterRatio: widget.diameterRatio,
......
......@@ -376,6 +376,7 @@ class NestedScrollView extends StatefulWidget {
this.dragStartBehavior = DragStartBehavior.start,
this.floatHeaderSlivers = false,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
}) : assert(scrollDirection != null),
assert(reverse != null),
assert(headerSliverBuilder != null),
......@@ -457,6 +458,9 @@ class NestedScrollView extends StatefulWidget {
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String restorationId;
/// Returns the [SliverOverlapAbsorberHandle] of the nearest ancestor
/// [NestedScrollView].
///
......@@ -636,6 +640,7 @@ class NestedScrollViewState extends State<NestedScrollView> {
),
handle: _absorberHandle,
clipBehavior: widget.clipBehavior,
restorationId: widget.restorationId,
);
},
),
......@@ -653,6 +658,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
@required this.handle,
@required this.clipBehavior,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
String restorationId,
}) : super(
scrollDirection: scrollDirection,
reverse: reverse,
......@@ -660,6 +666,7 @@ class _NestedScrollViewCustomScrollView extends CustomScrollView {
controller: controller,
slivers: slivers,
dragStartBehavior: dragStartBehavior,
restorationId: restorationId,
);
final SliverOverlapAbsorberHandle handle;
......
......@@ -390,6 +390,22 @@ class _PagePosition extends ScrollPositionWithSingleContext implements PageMetri
}
}
@override
void saveOffset() {
context.saveOffset(getPageFromPixels(pixels, viewportDimension));
}
@override
void restoreOffset(double offset, {bool initialRestore = false}) {
assert(initialRestore != null);
assert(offset != null);
if (initialRestore) {
_pageToUseOnStartup = offset;
} else {
jumpTo(getPixelsFromPage(offset));
}
}
@override
bool applyViewportDimension(double viewportDimension) {
final double oldViewportDimensions = this.viewportDimension;
......@@ -570,6 +586,7 @@ class PageView extends StatefulWidget {
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
}) : assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildListDelegate(children),
......@@ -605,6 +622,7 @@ class PageView extends StatefulWidget {
int itemCount,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
}) : assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
......@@ -703,6 +721,7 @@ class PageView extends StatefulWidget {
@required this.childrenDelegate,
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
}) : assert(childrenDelegate != null),
assert(allowImplicitScrolling != null),
controller = controller ?? _defaultPageController,
......@@ -721,6 +740,9 @@ class PageView extends StatefulWidget {
/// will traverse to the next page in the page view.
final bool allowImplicitScrolling;
/// {@macro flutter.widgets.scrollable.restorationId}
final String restorationId;
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
......@@ -824,6 +846,7 @@ class _PageViewState extends State<PageView> {
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
restorationId: widget.restorationId,
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
......
......@@ -35,6 +35,7 @@ abstract class ScrollContext {
/// particular, it should involve any [GlobalKey]s that are dynamically
/// created as part of creating the scrolling widget, since those would be
/// different each time the widget is created.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
BuildContext get storageContext;
/// A [TickerProvider] to use when animating the scroll position.
......@@ -61,4 +62,12 @@ abstract class ScrollContext {
/// Set the [SemanticsAction]s that should be expose to the semantics tree.
void setSemanticsActions(Set<SemanticsAction> actions);
/// Called by the [ScrollPosition] whenever scrolling ends to persist the
/// provided scroll `offset` for state restoration purposes.
///
/// The [ScrollContext] may pass the value back to a [ScrollPosition] by
/// calling [ScrollPosition.restoreOffset] at a later point in time or after
/// the application has restarted to restore the scroll offset.
void saveOffset(double offset);
}
......@@ -128,6 +128,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
///
/// * [ScrollController.keepScrollOffset] and [PageController.keepPage], which
/// create scroll positions and initialize this property.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
final bool keepScrollOffset;
/// A label that is used in the [toString] output.
......@@ -358,6 +359,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// The default implementation writes the [pixels] using the nearest
/// [PageStorage] found from the [context]'s [ScrollContext.storageContext]
/// property.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
@protected
void saveScrollOffset() {
PageStorage.of(context.storageContext)?.writeState(context.storageContext, pixels);
......@@ -378,6 +380,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
///
/// This method is called from the constructor, so layout has not yet
/// occurred, and the viewport dimensions aren't yet known when it is called.
// TODO(goderbauer): Deprecate this when state restoration supports all features of PageStorage.
@protected
void restoreScrollOffset() {
if (pixels == null) {
......@@ -387,6 +390,42 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
}
}
/// Called by [context] to restore the scroll offset to the provided value.
///
/// The provided value has previously been provided to the [context] by
/// calling [ScrollContext.saveOffset], e.g. from [saveOffset].
///
/// This method may be called right after the scroll position is created
/// before layout has occurred. In that case, `initialRestore` is set to true
/// and the viewport dimensions will not be known yet. If the [context]
/// doesn't have any information to restore the scroll offset this method is
/// not called.
///
/// The method may be called multiple times in the lifecycle of a
/// [ScrollPosition] to restore it to different scroll offsets.
void restoreOffset(double offset, {bool initialRestore = false}) {
assert(initialRestore != null);
assert(offset != null);
if (initialRestore) {
correctPixels(offset);
} else {
jumpTo(offset);
}
}
/// Called whenever scrolling ends, to persist the current scroll offset for
/// state restoration purposes.
///
/// The default implementation stores the current value of [pixels] on the
/// [context] by calling [ScrollContext.saveOffset]. At a later point in time
/// or after the application restarts, the [context] may restore the scroll
/// position to the persisted offset by calling [restoreOffset].
@protected
void saveOffset() {
assert(pixels != null);
context.saveOffset(pixels);
}
/// Returns the overscroll by applying the boundary conditions.
///
/// If the given value is in bounds, returns 0.0. Otherwise, returns the
......@@ -761,6 +800,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// This also saves the scroll offset using [saveScrollOffset].
void didEndScroll() {
activity.dispatchScrollEndNotification(copyWith(), context.notificationContext);
saveOffset();
if (keepScrollOffset)
saveScrollOffset();
}
......
......@@ -91,6 +91,7 @@ abstract class ScrollView extends StatelessWidget {
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
}) : assert(scrollDirection != null),
assert(reverse != null),
assert(shrinkWrap != null),
......@@ -260,6 +261,9 @@ abstract class ScrollView extends StatelessWidget {
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String restorationId;
/// Returns the [AxisDirection] in which the scroll view scrolls.
///
/// Combines the [scrollDirection] with the [reverse] boolean to obtain the
......@@ -333,6 +337,7 @@ abstract class ScrollView extends StatelessWidget {
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return buildViewport(context, offset, axisDirection, slivers);
},
......@@ -568,6 +573,7 @@ class CustomScrollView extends ScrollView {
this.slivers = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
String restorationId,
}) : super(
key: key,
scrollDirection: scrollDirection,
......@@ -581,6 +587,7 @@ class CustomScrollView extends ScrollView {
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
restorationId: restorationId,
);
/// The slivers to place inside the viewport.
......@@ -615,6 +622,7 @@ abstract class BoxScrollView extends ScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : super(
key: key,
scrollDirection: scrollDirection,
......@@ -627,6 +635,7 @@ abstract class BoxScrollView extends ScrollView {
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// The amount of space by which to inset the children.
......@@ -1027,6 +1036,7 @@ class ListView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
......@@ -1046,6 +1056,7 @@ class ListView extends BoxScrollView {
semanticChildCount: semanticChildCount ?? children.length,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, linear array of widgets that are created on demand.
......@@ -1098,6 +1109,7 @@ class ListView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount),
childrenDelegate = SliverChildBuilderDelegate(
......@@ -1120,6 +1132,7 @@ class ListView extends BoxScrollView {
semanticChildCount: semanticChildCount ?? itemCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a fixed-length scrollable linear array of list "items" separated
......@@ -1187,6 +1200,7 @@ class ListView extends BoxScrollView {
double cacheExtent,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(itemBuilder != null),
assert(separatorBuilder != null),
assert(itemCount != null && itemCount >= 0),
......@@ -1229,6 +1243,7 @@ class ListView extends BoxScrollView {
semanticChildCount: itemCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, linear array of widgets with a custom child model.
......@@ -1328,6 +1343,7 @@ class ListView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(childrenDelegate != null),
super(
key: key,
......@@ -1342,6 +1358,7 @@ class ListView extends BoxScrollView {
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// If non-null, forces the children to have the given extent in the scroll
......@@ -1621,6 +1638,7 @@ class GridView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildListDelegate(
children,
......@@ -1641,6 +1659,7 @@ class GridView extends BoxScrollView {
semanticChildCount: semanticChildCount ?? children.length,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, 2D array of widgets that are created on demand.
......@@ -1681,6 +1700,7 @@ class GridView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(gridDelegate != null),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
......@@ -1702,6 +1722,7 @@ class GridView extends BoxScrollView {
semanticChildCount: semanticChildCount ?? itemCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, 2D array of widgets with both a custom
......@@ -1726,6 +1747,7 @@ class GridView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : assert(gridDelegate != null),
assert(childrenDelegate != null),
super(
......@@ -1741,6 +1763,7 @@ class GridView extends BoxScrollView {
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, 2D array of widgets with a fixed number of tiles in
......@@ -1778,6 +1801,7 @@ class GridView extends BoxScrollView {
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainAxisSpacing,
......@@ -1803,6 +1827,7 @@ class GridView extends BoxScrollView {
semanticChildCount: semanticChildCount ?? children.length,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// Creates a scrollable, 2D array of widgets with tiles that each have a
......@@ -1835,10 +1860,12 @@ class GridView extends BoxScrollView {
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String restorationId,
}) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
mainAxisSpacing: mainAxisSpacing,
......@@ -1860,9 +1887,11 @@ class GridView extends BoxScrollView {
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount ?? children.length,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
);
/// A delegate that controls the layout of the children within the [GridView].
......
......@@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'actions.dart';
import 'basic.dart';
......@@ -19,6 +20,8 @@ import 'focus_manager.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'notification_listener.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart';
......@@ -90,6 +93,7 @@ class Scrollable extends StatefulWidget {
this.excludeFromSemantics = false,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.restorationId,
}) : assert(axisDirection != null),
assert(dragStartBehavior != null),
assert(viewportBuilder != null),
......@@ -226,6 +230,22 @@ class Scrollable extends StatefulWidget {
/// {@endtemplate}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scrollable.restorationId}
/// Restoration ID to save and restore the scroll offset of the scrollable.
///
/// If a restoration id is provided, the scrollable will persist its current
/// scroll offset and restore it during state restoration.
///
/// The scroll offset is persisted in a [RestorationBucket] claimed from
/// the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
/// {@endtemplate}
final String restorationId;
/// The axis along which the scroll view scrolls.
///
/// Determined by the [axisDirection].
......@@ -239,6 +259,7 @@ class Scrollable extends StatefulWidget {
super.debugFillProperties(properties);
properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
properties.add(DiagnosticsProperty<ScrollPhysics>('physics', physics));
properties.add(StringProperty('restorationId', restorationId));
}
/// The state from the closest instance of this class that encloses the given context.
......@@ -340,7 +361,7 @@ class _ScrollableScope extends InheritedWidget {
///
/// This class is not intended to be subclassed. To specialize the behavior of a
/// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
implements ScrollContext {
/// The manager for this [Scrollable] widget's viewport position.
///
......@@ -350,6 +371,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
ScrollPosition get position => _position;
ScrollPosition _position;
final _RestorableScrollOffset _persistedScrollOffset = _RestorableScrollOffset();
@override
AxisDirection get axisDirection => widget.axisDirection;
......@@ -378,10 +401,28 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
controller?.attach(position);
}
@override
void restoreState(RestorationBucket oldBucket) {
registerForRestoration(_persistedScrollOffset, 'offset');
assert(position != null);
if (_persistedScrollOffset.value != null) {
position.restoreOffset(_persistedScrollOffset.value, initialRestore: oldBucket == null);
}
}
@override
void saveOffset(double offset) {
assert(debugIsSerializableForRestoration(offset));
_persistedScrollOffset.value = offset;
// [saveOffset] is called after a scrolling ends and it is usually not
// followed by a frame. Therefore, manually flush restoration data.
ServicesBinding.instance.restorationManager.flushData();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updatePosition();
super.didChangeDependencies();
}
bool _shouldUpdatePosition(Scrollable oldWidget) {
......@@ -414,6 +455,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
void dispose() {
widget.controller?.detach(position);
position.dispose();
_persistedScrollOffset.dispose();
super.dispose();
}
......@@ -663,6 +705,9 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ScrollPosition>('position', position));
}
@override
String get restorationId => widget.restorationId;
}
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
......@@ -1029,3 +1074,28 @@ class ScrollAction extends Action<ScrollIntent> {
);
}
}
// Not using a RestorableDouble because we want to allow null values and override
// [enabled].
class _RestorableScrollOffset extends RestorableValue<double> {
@override
double createDefaultValue() => null;
@override
void didUpdateValue(double oldValue) {
notifyListeners();
}
@override
double fromPrimitives(Object data) {
return data as double;
}
@override
Object toPrimitives() {
return value;
}
@override
bool get enabled => value != null;
}
......@@ -222,6 +222,7 @@ class SingleChildScrollView extends StatelessWidget {
this.child,
this.dragStartBehavior = DragStartBehavior.start,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
}) : assert(scrollDirection != null),
assert(dragStartBehavior != null),
assert(clipBehavior != null),
......@@ -299,6 +300,9 @@ class SingleChildScrollView extends StatelessWidget {
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String restorationId;
AxisDirection _getDirection(BuildContext context) {
return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
}
......@@ -317,6 +321,7 @@ class SingleChildScrollView extends StatelessWidget {
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
restorationId: restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return _SingleChildViewport(
axisDirection: axisDirection,
......
......@@ -7,6 +7,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -197,6 +198,42 @@ void main() {
});
expect(rootBucket, isNull);
});
testWidgets('flushData', (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;
});
result.complete(_createEncodedRestorationData1());
await tester.pump();
expect(rootBucket, isNotNull);
callsToEngine.clear();
// Schedule a frame.
SchedulerBinding.instance.ensureVisualUpdate();
rootBucket.write('foo', 1);
// flushData is no-op because frame is scheduled.
manager.flushData();
expect(callsToEngine, isEmpty);
// Data is flushed at the end of the frame.
await tester.pump();
expect(callsToEngine, hasLength(1));
callsToEngine.clear();
// flushData without frame sends data directly.
rootBucket.write('foo', 2);
manager.flushData();
expect(callsToEngine, hasLength(1));
});
});
test('debugIsSerializableForRestoration', () {
......
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