Commit 7ba1879b authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Automatic silver keep alive (#11067)

* AutomaticKeepAlive

A Widget that listens for notifications from widgets that don't want to die.

* Automatically wrap SliverList and SliverGrid children in AutomaticKeepAlive widgets

* Fixes for review comments
parent 1438ae85
......@@ -238,12 +238,14 @@ class InkResponse extends StatefulWidget {
}
}
class _InkResponseState<T extends InkResponse> extends State<T> {
class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKeepAliveClientMixin {
Set<InkSplash> _splashes;
InkSplash _currentSplash;
InkHighlight _lastHighlight;
@override
bool get wantKeepAlive => _lastHighlight != null || (_splashes != null && _splashes.isNotEmpty);
void updateHighlight(bool value) {
if (value == (_lastHighlight != null && _lastHighlight.active))
return;
......@@ -260,8 +262,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
onRemoved: () {
assert(_lastHighlight != null);
_lastHighlight = null;
updateKeepAlive();
},
);
updateKeepAlive();
} else {
_lastHighlight.activate();
}
......@@ -292,12 +296,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
_splashes.remove(splash);
if (_currentSplash == splash)
_currentSplash = null;
updateKeepAlive();
} // else we're probably in deactivate()
}
);
_splashes ??= new HashSet<InkSplash>();
_splashes.add(splash);
_currentSplash = splash;
updateKeepAlive();
updateHighlight(true);
}
......@@ -353,6 +359,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
@override
Widget build(BuildContext context) {
assert(widget.debugCheckContext(context));
super.build(context); // See AutomaticKeepAliveClientMixin.
final ThemeData themeData = Theme.of(context);
_lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor;
_currentSplash?.color = widget.splashColor ?? themeData.splashColor;
......
......@@ -12,6 +12,11 @@ import 'theme.dart';
const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn;
// See the InputDecorator.build method, where this is used.
class _InputDecoratorChildGlobalKey extends GlobalObjectKey {
const _InputDecoratorChildGlobalKey(BuildContext value) : super(value);
}
/// Text and styles used to label an input field.
///
/// See also:
......@@ -482,7 +487,19 @@ class InputDecorator extends StatelessWidget {
);
}
Widget inputChild;
Widget inputChild = new KeyedSubtree(
// It's important that we maintain the state of our child subtree, as it
// may be stateful (e.g. containing text selections). Since our build
// function risks changing the depth of the tree, we preserve the subtree
// using global keys.
// GlobalObjectKey(context) will always be the same whenever we are built.
// Additionally, we use a subclass of GlobalObjectKey to avoid clashes
// with anyone else using our BuildContext as their global object key
// value.
key: new _InputDecoratorChildGlobalKey(context),
child: child,
);
if (!hasInlineLabel && (!isEmpty || hintText == null) &&
(decoration?.prefixText != null || decoration?.suffixText != null)) {
final List<Widget> rowContents = <Widget>[];
......@@ -492,7 +509,7 @@ class InputDecorator extends StatelessWidget {
style: decoration.prefixStyle ?? hintStyle)
);
}
rowContents.add(new Expanded(child: child));
rowContents.add(new Expanded(child: inputChild));
if (decoration.suffixText != null) {
rowContents.add(
new Text(decoration.suffixText,
......@@ -500,8 +517,6 @@ class InputDecorator extends StatelessWidget {
);
}
inputChild = new Row(children: rowContents);
} else {
inputChild = child;
}
if (isCollapsed) {
......
......@@ -294,24 +294,26 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override
Color color;
final List<InkFeature> _inkFeatures = <InkFeature>[];
List<InkFeature> _inkFeatures;
@override
void addInkFeature(InkFeature feature) {
assert(!feature._debugDisposed);
assert(feature._controller == this);
_inkFeatures ??= <InkFeature>[];
assert(!_inkFeatures.contains(feature));
_inkFeatures.add(feature);
markNeedsPaint();
}
void _removeFeature(InkFeature feature) {
assert(_inkFeatures != null);
_inkFeatures.remove(feature);
markNeedsPaint();
}
void _didChangeLayout() {
if (_inkFeatures.isNotEmpty)
if (_inkFeatures != null && _inkFeatures.isNotEmpty)
markNeedsPaint();
}
......@@ -320,7 +322,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override
void paint(PaintingContext context, Offset offset) {
if (_inkFeatures.isNotEmpty) {
if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas;
canvas.save();
canvas.translate(offset.dx, offset.dy);
......@@ -334,7 +336,12 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
}
class _InkFeatures extends SingleChildRenderObjectWidget {
const _InkFeatures({ Key key, this.color, Widget child, @required this.vsync }) : super(key: key, child: child);
const _InkFeatures({
Key key,
this.color,
@required this.vsync,
Widget child,
}) : super(key: key, child: child);
// This widget must be owned by a MaterialState, which must be provided as the vsync.
// This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
......@@ -347,7 +354,7 @@ class _InkFeatures extends SingleChildRenderObjectWidget {
_RenderInkFeatures createRenderObject(BuildContext context) {
return new _RenderInkFeatures(
color: color,
vsync: vsync
vsync: vsync,
);
}
......@@ -368,7 +375,7 @@ abstract class InkFeature {
InkFeature({
@required MaterialInkController controller,
@required this.referenceBox,
this.onRemoved
this.onRemoved,
}) : assert(controller != null),
assert(referenceBox != null),
_controller = controller;
......
This diff is collapsed.
......@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
......@@ -164,7 +165,7 @@ class _DismissibleClipper extends CustomClipper<Rect> {
}
}
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin {
class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
@override
void initState() {
super.initState();
......@@ -183,6 +184,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
bool _dragUnderway = false;
Size _sizePriorToCollapse;
@override
bool get wantKeepAlive => _moveController?.isAnimating == true || _resizeController?.isAnimating == true;
@override
void dispose() {
_moveController.dispose();
......@@ -323,6 +327,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
void _handleDismissStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed && !_dragUnderway)
_startResizeAnimation();
updateKeepAlive();
}
void _startResizeAnimation() {
......@@ -335,7 +340,8 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
widget.onDismissed(_dismissDirection);
} else {
_resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged);
..addListener(_handleResizeProgressChanged)
..addStatusListener((AnimationStatus status) => updateKeepAlive());
_resizeController.forward();
setState(() {
_sizePriorToCollapse = context.size;
......@@ -362,6 +368,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
@override
Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
Widget background = widget.background;
if (widget.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection;
......
......@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
......@@ -252,7 +253,7 @@ class EditableText extends StatefulWidget {
description.add('focusNode: $focusNode');
if (obscureText != false)
description.add('obscureText: $obscureText');
description.add('$style');
description.add('${style.toString().split("\n").join(", ")}');
if (textAlign != null)
description.add('$textAlign');
if (textScaleFactor != null)
......@@ -267,7 +268,7 @@ class EditableText extends StatefulWidget {
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> implements TextInputClient {
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient {
Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
......@@ -278,6 +279,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
final LayerLink _layerLink = new LayerLink();
bool _didAutoFocus = false;
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
// State lifecycle:
@override
......@@ -308,6 +312,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
}
}
......@@ -549,11 +554,13 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
// Clear the selection and composition state if this widget lost focus.
_value = new TextEditingValue(text: _value.text);
}
updateKeepAlive();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin.
return new Scrollable(
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,
......
......@@ -343,6 +343,22 @@ class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
/// Used to tie the identity of a widget to the identity of an object used to
/// generate that widget.
///
/// If the object is not private, then it is possible that collisions will occur
/// where independent widgets will reuse the same object as their
/// [GlobalObjectKey] value in a different part of the tree, leading to a global
/// key conflict. To avoid this problem, create a private [GlobalObjectKey]
/// subclass, as in:
///
/// ```dart
/// class _MyKey extends GlobalObjectKey {
/// const _MyKey(Object value) : super(value);
/// }
/// ```
///
/// Since the [runtimeType] of the key is part of its identity, this will
/// prevent clashes with other [GlobalObjectKey]s even if they have the same
/// value.
///
/// Any [GlobalObjectKey] created for the same value will match.
@optionalTypeArgs
class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
......@@ -1418,6 +1434,19 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
/// parent, as appropriate.
@protected
void applyParentData(RenderObject renderObject);
/// Whether the [ParentDataElement.applyWidgetOutOfTurn] method is allowed
/// with this widget.
///
/// This should only return true if this widget represents a [ParentData]
/// configuration that will have no impact on the layout or paint phase.
///
/// See also:
///
/// * [ParentDataElement.applyWidgetOutOfTurn], which verifies this in debug
/// mode.
@protected
bool debugCanApplyOutOfTurn() => false;
}
/// Base class for widgets that efficiently propagate information down the tree.
......@@ -2470,6 +2499,11 @@ abstract class Element implements BuildContext {
///
/// There is no guaranteed order in which the children will be visited, though
/// it should be consistent over time.
///
/// Calling this during build is dangerous: the child list might still be
/// being updated at that point, so the children might not be constructed yet,
/// or might be old children that are going to be replaced. This method should
/// only be called if it is provable that the children are available.
void visitChildren(ElementVisitor visitor) { }
/// Calls the argument for each child that is relevant for semantics. By
......@@ -2477,11 +2511,20 @@ abstract class Element implements BuildContext {
/// to hide their children.
void visitChildrenForSemantics(ElementVisitor visitor) => visitChildren(visitor);
/// Wrapper around visitChildren for BuildContext.
/// Wrapper around [visitChildren] for [BuildContext].
@override
void visitChildElements(ElementVisitor visitor) {
// don't allow visitChildElements() during build, since children aren't necessarily built yet
assert(owner == null || !owner._debugStateLocked);
assert(() {
if (owner == null || !owner._debugStateLocked)
return true;
throw new FlutterError(
'visitChildElements() called during build.\n'
'The BuildContext.visitChildElements() method can\'t be called during '
'build because the child list is still being updated at that point, '
'so the children might not be constructed yet, or might be old children '
'that are going to be replaced.'
);
});
visitChildren(visitor);
}
......@@ -3685,18 +3728,60 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement {
super.mount(parent, slot);
}
void _notifyChildren(Element child) {
if (child is RenderObjectElement) {
child._updateParentData(widget);
} else {
assert(child is! ParentDataElement<RenderObjectWidget>);
child.visitChildren(_notifyChildren);
void _applyParentData(ParentDataWidget<T> widget) {
void applyParentDataToChild(Element child) {
if (child is RenderObjectElement) {
child._updateParentData(widget);
} else {
assert(child is! ParentDataElement<RenderObjectWidget>);
child.visitChildren(applyParentDataToChild);
}
}
visitChildren(applyParentDataToChild);
}
/// Calls [ParentDataWidget.applyParentData] on the given widget, passing it
/// the [RenderObject] whose parent data this element is ultimately
/// responsible for.
///
/// This allows a render object's [RenderObject.parentData] to be modified
/// without triggering a build. This is generally ill-advised, but makes sense
/// in situations such as the following:
///
/// * Build and layout are currently under way, but the [ParentData] in question
/// does not affect layout, and the value to be applied could not be
/// determined before build and layout (e.g. it depends on the layout of a
/// descendant).
///
/// * Paint is currently under way, but the [ParentData] in question does not
/// affect layour or paint, and the value to be applied could not be
/// determined before paint (e.g. it depends on the compositing phase).
///
/// In either case, the next build is expected to cause this element to be
/// configured with the given new widget (or a widget with equivalent data).
///
/// Only [ParentDataWidget]s that return true for
/// [ParentDataWidget.debugCanApplyOutOfTurn] can be applied this way.
///
/// The new widget must have the same child as the current widget.
///
/// An example of when this is used is the [AutomaticKeepAlive] widget. If it
/// receives a notification during the build of one of its descendants saying
/// that its child must be kept alive, it will apply a [KeepAlive] widget out
/// of turn. This is safe, because by definition the child is already alive,
/// and therefore this will not change the behavior of the parent this frame.
/// It is more efficient than requesting an additional frame just for the
/// purpose of updating the [KeepAlive] widget.
void applyWidgetOutOfTurn(ParentDataWidget<T> newWidget) {
assert(newWidget != null);
assert(newWidget.debugCanApplyOutOfTurn());
assert(newWidget.child == widget.child);
_applyParentData(newWidget);
}
@override
void notifyClients(ParentDataWidget<T> oldWidget) {
visitChildren(_notifyChildren);
_applyParentData(widget);
}
}
......
......@@ -26,6 +26,8 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification
/// widgets with the appropriate type parameters that are ancestors of the given
/// [BuildContext].
abstract class Notification {
const Notification();
/// Applied to each ancestor of the [dispatch] target.
///
/// The [Notification] class implementation of this method dispatches the
......@@ -89,7 +91,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
const NotificationListener({
Key key,
@required this.child,
this.onNotification
this.onNotification,
}) : super(key: key);
/// The widget below this widget in the tree.
......
......@@ -515,8 +515,10 @@ class ListView extends BoxScrollView {
/// It is usually more efficient to create children on demand using [new
/// ListView.builder].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null.
ListView({
Key key,
......@@ -528,10 +530,12 @@ class ListView extends BoxScrollView {
bool shrinkWrap: false,
EdgeInsets padding,
this.itemExtent,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
......@@ -563,9 +567,11 @@ class ListView extends BoxScrollView {
/// Even more efficient, however, is to create the instances on demand using
/// this constructor's `itemBuilder` callback.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property. Both must not
/// be null.
ListView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -578,10 +584,12 @@ class ListView extends BoxScrollView {
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
}) : childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
......@@ -783,8 +791,10 @@ class GridView extends BoxScrollView {
///
/// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null.
GridView({
Key key,
......@@ -796,11 +806,13 @@ class GridView extends BoxScrollView {
bool shrinkWrap: false,
EdgeInsets padding,
@required this.gridDelegate,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : assert(gridDelegate != null),
childrenDelegate = new SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
super(
......@@ -828,9 +840,11 @@ class GridView extends BoxScrollView {
///
/// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property. Both must not
/// be null.
GridView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -843,11 +857,13 @@ class GridView extends BoxScrollView {
@required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
}) : assert(gridDelegate != null),
childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
),
super(
......@@ -897,8 +913,10 @@ class GridView extends BoxScrollView {
///
/// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null.
///
/// See also:
......@@ -917,6 +935,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
......@@ -927,6 +946,7 @@ class GridView extends BoxScrollView {
),
childrenDelegate = new SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
......@@ -944,8 +964,10 @@ class GridView extends BoxScrollView {
///
/// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null.
///
/// See also:
......@@ -964,6 +986,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
......@@ -974,6 +997,7 @@ class GridView extends BoxScrollView {
),
childrenDelegate = new SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
......
......@@ -7,6 +7,7 @@ import 'dart:collection' show SplayTreeMap, HashMap;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'framework.dart';
......@@ -43,7 +44,7 @@ abstract class SliverChildDelegate {
/// exists.
///
/// Subclasses typically override this function and wrap their children in
/// [RepaintBoundary] widgets.
/// [AutomaticKeepAlive] and [RepaintBoundary] widgets.
Widget build(BuildContext context, int index);
/// Returns an estimate of the number of children this delegate will build.
......@@ -119,7 +120,9 @@ abstract class SliverChildDelegate {
/// not even have to be built until they are displayed.
///
/// The widgets returned from the builder callback are automatically wrapped in
/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
/// [AutomaticKeepAlive] widgets if [addAutomaticKeepAlives] is true (the
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// See also:
///
......@@ -129,12 +132,15 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// builder callback.
///
/// The [builder] and [addRepaintBoundaries] arguments must not be null.
/// The [builder], [addAutomaticKeepAlives], and [addRepaintBoundaries]
/// arguments must not be null.
const SliverChildBuilderDelegate(
this.builder, {
this.childCount,
this.addAutomaticKeepAlives: true,
this.addRepaintBoundaries: true,
}) : assert(builder != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
/// Called to build children for the sliver.
......@@ -155,6 +161,20 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// [builder] returns null.
final int childCount;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
......@@ -171,10 +191,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount))
return null;
final Widget child = builder(context, index);
Widget child = builder(context, index);
if (child == null)
return null;
return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
if (addRepaintBoundaries)
child = new RepaintBoundary.wrap(child, index);
if (addAutomaticKeepAlives)
child = new AutomaticKeepAlive(child: child);
return child;
}
@override
......@@ -205,7 +229,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// conditions.
///
/// The widgets in the given [children] list are automatically wrapped in
/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
/// [AutomaticKeepAlive] widgets if [addAutomaticKeepAlives] is true (the
/// default) and in [RepaintBoundary] widgets if [addRepaintBoundaries] is true
/// (also the default).
///
/// See also:
///
......@@ -215,13 +241,30 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// list.
///
/// The [children] and [addRepaintBoundaries] arguments must not be null.
/// The [children], [addAutomaticKeepAlives], and [addRepaintBoundaries]
/// arguments must not be null.
const SliverChildListDelegate(
this.children, {
this.addAutomaticKeepAlives: true,
this.addRepaintBoundaries: true,
}) : assert(children != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null);
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
......@@ -241,9 +284,13 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(children != null);
if (index < 0 || index >= children.length)
return null;
final Widget child = children[index];
Widget child = children[index];
assert(child != null);
return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
if (addRepaintBoundaries)
child = new RepaintBoundary.wrap(child, index);
if (addAutomaticKeepAlives)
child = new AutomaticKeepAlive(child: child);
return child;
}
@override
......@@ -875,11 +922,18 @@ class KeepAlive extends ParentDataWidget<SliverMultiBoxAdaptorWidget> {
if (parentData.keepAlive != keepAlive) {
parentData.keepAlive = keepAlive;
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject)
targetParent.markNeedsLayout();
if (targetParent is RenderObject && !keepAlive)
targetParent.markNeedsLayout(); // No need to redo layout if it became true.
}
}
// We only return true if [keepAlive] is true, because turning _off_ keep
// alive requires a layout to do the garbage collection (but turning it on
// requires nothing, since by definition the widget is already alive and won't
// go away _unless_ we do a layout).
@override
bool debugCanApplyOutOfTurn() => keepAlive;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
......
......@@ -19,6 +19,7 @@ export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart';
export 'src/widgets/app.dart';
export 'src/widgets/async.dart';
export 'src/widgets/automatic_keep_alive.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
......
......@@ -3,8 +3,10 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import 'feedback_tester.dart';
void main() {
......@@ -116,4 +118,37 @@ void main() {
expect(feedback.hapticCount, 0);
});
});
testWidgets('splashing survives scrolling when keep-alive is enabled', (WidgetTester tester) async {
Future<Null> runTest(bool keepAlive) async {
await tester.pumpWidget(new Material(
child: new CompositedTransformFollower( // forces a layer, which makes the paints easier to separate out
link: new LayerLink(),
child: new ListView(
addAutomaticKeepAlives: keepAlive,
children: <Widget>[
new Container(height: 500.0, child: new InkWell(onTap: () { }, child: const Placeholder())),
new Container(height: 500.0),
new Container(height: 500.0),
],
),
),
));
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, paintsNothing);
await tester.tap(find.byType(InkWell));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child, paints..circle());
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump(const Duration(milliseconds: 10));
await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0));
await tester.pump(const Duration(milliseconds: 10));
expect(
tester.renderObject<RenderProxyBox>(find.byType(PhysicalModel)).child,
keepAlive ? (paints..circle()) : paintsNothing,
);
}
await runTest(true);
await runTest(false);
});
}
......@@ -132,4 +132,78 @@ void main() {
expect(tester.testTextInput.isVisible, isFalse);
});
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
final FocusNode focusNode = new FocusNode();
await tester.pumpWidget(
new MaterialApp(
home: new Material(
child: new ListView(
children: <Widget>[
new TextField(
focusNode: focusNode,
),
new Container(
height: 1000.0,
),
],
),
),
),
);
expect(find.byType(TextField), findsOneWidget);
expect(tester.testTextInput.isVisible, isFalse);
FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
await tester.pump();
expect(find.byType(TextField), findsOneWidget);
expect(tester.testTextInput.isVisible, isTrue);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump();
expect(find.byType(TextField), findsOneWidget);
expect(tester.testTextInput.isVisible, isTrue);
focusNode.unfocus();
await tester.pump();
expect(find.byType(TextField), findsNothing);
expect(tester.testTextInput.isVisible, isFalse);
});
testWidgets('Focus keep-alive works with GlobalKey reparenting', (WidgetTester tester) async {
final FocusNode focusNode = new FocusNode();
Widget makeTest(String prefix) {
return new MaterialApp(
home: new Material(
child: new ListView(
children: <Widget>[
new TextField(
focusNode: focusNode,
decoration: new InputDecoration(
prefixText: prefix,
),
),
new Container(
height: 1000.0,
),
],
),
),
);
}
await tester.pumpWidget(makeTest(null));
FocusScope.of(tester.element(find.byType(TextField))).requestFocus(focusNode);
await tester.pump();
expect(find.byType(TextField), findsOneWidget);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump();
expect(find.byType(TextField), findsOneWidget);
await tester.pumpWidget(makeTest('test'));
await tester.pump(); // in case the AutomaticKeepAlive widget thinks it needs a cleanup frame
expect(find.byType(TextField), findsOneWidget);
});
}
......@@ -36,8 +36,13 @@ import 'recording_canvas.dart';
/// order).
///
/// See [PaintPattern] for a discussion of the semantics of paint patterns.
///
/// To match something which paints nothing, see [paintsNothing].
PaintPattern get paints => new _TestRecordingCanvasPatternMatcher();
/// Matches objects or functions that paint an empty display list.
Matcher get paintsNothing => new _TestRecordingCanvasPaintsNothingMatcher();
/// Signature for [PaintPattern.something] predicate argument.
///
/// Used by the [paints] matcher.
......@@ -243,7 +248,73 @@ abstract class PaintPattern {
void something(PaintPatternPredicate predicate);
}
class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern {
abstract class _TestRecordingCanvasMatcher extends Matcher {
@override
bool matches(Object object, Map<dynamic, dynamic> matchState) {
final TestRecordingCanvas canvas = new TestRecordingCanvas();
final TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas);
if (object is _ContextPainterFunction) {
final _ContextPainterFunction function = object;
function(context, Offset.zero);
} else if (object is _CanvasPainterFunction) {
final _CanvasPainterFunction function = object;
function(canvas);
} else {
if (object is Finder) {
TestAsyncUtils.guardSync();
final Finder finder = object;
object = finder.evaluate().single.renderObject;
}
if (object is RenderObject) {
final RenderObject renderObject = object;
renderObject.paint(context, Offset.zero);
} else {
matchState[this] = 'was not one of the supported objects for the "paints" matcher.';
return false;
}
}
final StringBuffer description = new StringBuffer();
final bool result = _evaluatePredicates(canvas.invocations, description);
if (!result) {
const String indent = '\n '; // the length of ' Which: ' in spaces, plus two more
if (canvas.invocations.isNotEmpty)
description.write(' The complete display list was:');
for (Invocation call in canvas.invocations)
description.write('$indent${_describeInvocation(call)}');
}
matchState[this] = description.toString();
return result;
}
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description);
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return description.add(matchState[this]);
}
}
class _TestRecordingCanvasPaintsNothingMatcher extends _TestRecordingCanvasMatcher {
@override
Description describe(Description description) {
return description.add('An object or closure that paints nothing.');
}
@override
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description) {
if (calls.isEmpty)
return true;
description.write('painted the following.');
return false;
}
}
class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher implements PaintPattern {
final List<_PaintPredicate> _predicates = <_PaintPredicate>[];
@override
......@@ -326,43 +397,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
_predicates.add(new _SomethingPaintPredicate(predicate));
}
@override
bool matches(Object object, Map<dynamic, dynamic> matchState) {
final TestRecordingCanvas canvas = new TestRecordingCanvas();
final TestRecordingPaintingContext context = new TestRecordingPaintingContext(canvas);
if (object is _ContextPainterFunction) {
final _ContextPainterFunction function = object;
function(context, Offset.zero);
} else if (object is _CanvasPainterFunction) {
final _CanvasPainterFunction function = object;
function(canvas);
} else {
if (object is Finder) {
TestAsyncUtils.guardSync();
final Finder finder = object;
object = finder.evaluate().single.renderObject;
}
if (object is RenderObject) {
final RenderObject renderObject = object;
renderObject.paint(context, Offset.zero);
} else {
matchState[this] = 'was not one of the supported objects for the "paints" matcher.';
return false;
}
}
final StringBuffer description = new StringBuffer();
final bool result = _evaluatePredicates(canvas.invocations, description);
if (!result) {
const String indent = '\n '; // the length of ' Which: ' in spaces, plus two more
if (canvas.invocations.isNotEmpty)
description.write(' The complete display list was:');
for (Invocation call in canvas.invocations)
description.write('$indent${_describeInvocation(call)}');
}
matchState[this] = description.toString();
return result;
}
@override
Description describe(Description description) {
if (_predicates.isEmpty)
......@@ -375,18 +409,11 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
}
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return description.add(matchState[this]);
}
bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description) {
// If we ever want to have a matcher for painting nothing, create a separate
// paintsNothing matcher.
if (calls.isEmpty) {
description.write('painted nothing.');
return false;
}
if (_predicates.isEmpty) {
description.write(
'painted something, but you must now add a pattern to the paints matcher '
......@@ -394,10 +421,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
);
return false;
}
if (calls.isEmpty) {
description.write('painted nothing.');
return false;
}
final Iterator<_PaintPredicate> predicate = _predicates.iterator;
final Iterator<Invocation> call = calls.iterator..moveNext();
try {
......
This diff is collapsed.
......@@ -8,8 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class Leaf extends StatefulWidget {
Leaf({ Key key, this.index, this.child }) : super(key: key);
final int index;
Leaf({ Key key, this.child }) : super(key: key);
final Widget child;
@override
_LeafState createState() => new _LeafState();
......@@ -36,7 +35,6 @@ List<Widget> generateList(Widget child) {
100,
(int index) => new Leaf(
key: new GlobalObjectKey<_LeafState>(index),
index: index,
child: child,
),
growable: false,
......@@ -46,6 +44,7 @@ List<Widget> generateList(Widget child) {
void main() {
testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()),
......@@ -85,6 +84,7 @@ void main() {
testWidgets('KeepAlive with ListView without itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
children: generateList(new Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
));
......@@ -123,6 +123,7 @@ void main() {
testWidgets('KeepAlive with GridView', (WidgetTester tester) async {
await tester.pumpWidget(new GridView.count(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
......@@ -163,6 +164,7 @@ void main() {
testWidgets('KeepAlive render tree description', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()),
......
......@@ -260,6 +260,7 @@ void main() {
testWidgets('ListView underflow extents', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
children: <Widget>[
new Container(height: 100.0),
new Container(height: 100.0),
......
......@@ -15,7 +15,7 @@ void main() {
new CustomScrollView(
slivers: <Widget>[
new SliverFillViewport(
delegate: new SliverChildListDelegate(children),
delegate: new SliverChildListDelegate(children, addAutomaticKeepAlives: false),
),
],
),
......
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