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 { ...@@ -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; Set<InkSplash> _splashes;
InkSplash _currentSplash; InkSplash _currentSplash;
InkHighlight _lastHighlight; InkHighlight _lastHighlight;
@override
bool get wantKeepAlive => _lastHighlight != null || (_splashes != null && _splashes.isNotEmpty);
void updateHighlight(bool value) { void updateHighlight(bool value) {
if (value == (_lastHighlight != null && _lastHighlight.active)) if (value == (_lastHighlight != null && _lastHighlight.active))
return; return;
...@@ -260,8 +262,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -260,8 +262,10 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
onRemoved: () { onRemoved: () {
assert(_lastHighlight != null); assert(_lastHighlight != null);
_lastHighlight = null; _lastHighlight = null;
updateKeepAlive();
}, },
); );
updateKeepAlive();
} else { } else {
_lastHighlight.activate(); _lastHighlight.activate();
} }
...@@ -292,12 +296,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -292,12 +296,14 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
_splashes.remove(splash); _splashes.remove(splash);
if (_currentSplash == splash) if (_currentSplash == splash)
_currentSplash = null; _currentSplash = null;
updateKeepAlive();
} // else we're probably in deactivate() } // else we're probably in deactivate()
} }
); );
_splashes ??= new HashSet<InkSplash>(); _splashes ??= new HashSet<InkSplash>();
_splashes.add(splash); _splashes.add(splash);
_currentSplash = splash; _currentSplash = splash;
updateKeepAlive();
updateHighlight(true); updateHighlight(true);
} }
...@@ -353,6 +359,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> { ...@@ -353,6 +359,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(widget.debugCheckContext(context)); assert(widget.debugCheckContext(context));
super.build(context); // See AutomaticKeepAliveClientMixin.
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
_lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor; _lastHighlight?.color = widget.highlightColor ?? themeData.highlightColor;
_currentSplash?.color = widget.splashColor ?? themeData.splashColor; _currentSplash?.color = widget.splashColor ?? themeData.splashColor;
......
...@@ -12,6 +12,11 @@ import 'theme.dart'; ...@@ -12,6 +12,11 @@ import 'theme.dart';
const Duration _kTransitionDuration = const Duration(milliseconds: 200); const Duration _kTransitionDuration = const Duration(milliseconds: 200);
const Curve _kTransitionCurve = Curves.fastOutSlowIn; 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. /// Text and styles used to label an input field.
/// ///
/// See also: /// See also:
...@@ -482,7 +487,19 @@ class InputDecorator extends StatelessWidget { ...@@ -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) && if (!hasInlineLabel && (!isEmpty || hintText == null) &&
(decoration?.prefixText != null || decoration?.suffixText != null)) { (decoration?.prefixText != null || decoration?.suffixText != null)) {
final List<Widget> rowContents = <Widget>[]; final List<Widget> rowContents = <Widget>[];
...@@ -492,7 +509,7 @@ class InputDecorator extends StatelessWidget { ...@@ -492,7 +509,7 @@ class InputDecorator extends StatelessWidget {
style: decoration.prefixStyle ?? hintStyle) style: decoration.prefixStyle ?? hintStyle)
); );
} }
rowContents.add(new Expanded(child: child)); rowContents.add(new Expanded(child: inputChild));
if (decoration.suffixText != null) { if (decoration.suffixText != null) {
rowContents.add( rowContents.add(
new Text(decoration.suffixText, new Text(decoration.suffixText,
...@@ -500,8 +517,6 @@ class InputDecorator extends StatelessWidget { ...@@ -500,8 +517,6 @@ class InputDecorator extends StatelessWidget {
); );
} }
inputChild = new Row(children: rowContents); inputChild = new Row(children: rowContents);
} else {
inputChild = child;
} }
if (isCollapsed) { if (isCollapsed) {
......
...@@ -294,24 +294,26 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -294,24 +294,26 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override @override
Color color; Color color;
final List<InkFeature> _inkFeatures = <InkFeature>[]; List<InkFeature> _inkFeatures;
@override @override
void addInkFeature(InkFeature feature) { void addInkFeature(InkFeature feature) {
assert(!feature._debugDisposed); assert(!feature._debugDisposed);
assert(feature._controller == this); assert(feature._controller == this);
_inkFeatures ??= <InkFeature>[];
assert(!_inkFeatures.contains(feature)); assert(!_inkFeatures.contains(feature));
_inkFeatures.add(feature); _inkFeatures.add(feature);
markNeedsPaint(); markNeedsPaint();
} }
void _removeFeature(InkFeature feature) { void _removeFeature(InkFeature feature) {
assert(_inkFeatures != null);
_inkFeatures.remove(feature); _inkFeatures.remove(feature);
markNeedsPaint(); markNeedsPaint();
} }
void _didChangeLayout() { void _didChangeLayout() {
if (_inkFeatures.isNotEmpty) if (_inkFeatures != null && _inkFeatures.isNotEmpty)
markNeedsPaint(); markNeedsPaint();
} }
...@@ -320,7 +322,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -320,7 +322,7 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (_inkFeatures.isNotEmpty) { if (_inkFeatures != null && _inkFeatures.isNotEmpty) {
final Canvas canvas = context.canvas; final Canvas canvas = context.canvas;
canvas.save(); canvas.save();
canvas.translate(offset.dx, offset.dy); canvas.translate(offset.dx, offset.dy);
...@@ -334,7 +336,12 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController ...@@ -334,7 +336,12 @@ class _RenderInkFeatures extends RenderProxyBox implements MaterialInkController
} }
class _InkFeatures extends SingleChildRenderObjectWidget { 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 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. // This relationship must be 1:1 and cannot change for the lifetime of the MaterialState.
...@@ -347,7 +354,7 @@ class _InkFeatures extends SingleChildRenderObjectWidget { ...@@ -347,7 +354,7 @@ class _InkFeatures extends SingleChildRenderObjectWidget {
_RenderInkFeatures createRenderObject(BuildContext context) { _RenderInkFeatures createRenderObject(BuildContext context) {
return new _RenderInkFeatures( return new _RenderInkFeatures(
color: color, color: color,
vsync: vsync vsync: vsync,
); );
} }
...@@ -368,7 +375,7 @@ abstract class InkFeature { ...@@ -368,7 +375,7 @@ abstract class InkFeature {
InkFeature({ InkFeature({
@required MaterialInkController controller, @required MaterialInkController controller,
@required this.referenceBox, @required this.referenceBox,
this.onRemoved this.onRemoved,
}) : assert(controller != null), }) : assert(controller != null),
assert(referenceBox != null), assert(referenceBox != null),
_controller = controller; _controller = controller;
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'framework.dart';
import 'notification_listener.dart';
import 'sliver.dart';
/// Allows subtrees to request to be kept alive in lazy lists.
///
/// This widget is like [KeepAlive] but instead of being explicitly configured,
/// it listens to [KeepAliveNotification] messages from the [child] and other
/// descendants.
///
/// The subtree is kept alive whenever there is one or more descendant that has
/// sent a [KeepAliveNotification] and not yet triggered its
/// [KeepAliveNotification.handle].
///
/// To send these notifications, consider using [AutomaticKeepAliveClientMixin].
class AutomaticKeepAlive extends StatefulWidget {
/// Creates a widget that listens to [KeepAliveNotification]s and maintains a
/// [KeepAlive] widget appropriately.
const AutomaticKeepAlive({
Key key,
this.child,
}) : super(key: key);
/// The widget below this widget in the tree.
final Widget child;
@override
_AutomaticKeepAliveState createState() => new _AutomaticKeepAliveState();
}
class _AutomaticKeepAliveState extends State<AutomaticKeepAlive> {
Map<Listenable, VoidCallback> _handles;
Widget _child;
bool _keepingAlive = false;
@override
void initState() {
super.initState();
_updateChild();
}
@override
void didUpdateWidget(AutomaticKeepAlive oldWidget) {
super.didUpdateWidget(oldWidget);
_updateChild();
}
void _updateChild() {
_child = new NotificationListener<KeepAliveNotification>(
onNotification: _addClient,
child: widget.child,
);
}
@override
void dispose() {
if (_handles != null) {
for (Listenable handle in _handles.keys)
handle.removeListener(_handles[handle]);
}
super.dispose();
}
bool _addClient(KeepAliveNotification notification) {
final Listenable handle = notification.handle;
_handles ??= <Listenable, VoidCallback>{};
assert(!_handles.containsKey(handle));
_handles[handle] = _createCallback(handle);
handle.addListener(_handles[handle]);
if (!_keepingAlive) {
_keepingAlive = true;
// We use Element.visitChildren rather than context.visitChildElements
// because we might be called during build, and context.visitChildElements
// verifies that it is not called during build. Element.visitChildren does
// not, instead it assumes that the caller will be careful. (See the
// documentation for these methods for more details.)
//
// Here we know it's safe because we just received a notification, which
// we wouldn't be able to do if we hadn't built our child and its child --
// our build method always builds the same subtree and it always includes
// the node we're looking for (KeepAlive) as the parent of the node that
// reports the notifications (NotificationListener).
//
// (We're only going down one level, to get our direct child.)
final Element element = context;
element.visitChildren((Element child) {
assert(child is ParentDataElement<SliverMultiBoxAdaptorWidget>);
final ParentDataElement<SliverMultiBoxAdaptorWidget> childElement = child;
childElement.applyWidgetOutOfTurn(build(context));
});
}
return false;
}
VoidCallback _createCallback(Listenable handle) {
return () {
assert(() {
if (!mounted) {
throw new FlutterError(
'AutomaticKeepAlive handle triggered after AutomaticKeepAlive was disposed.'
'Widgets should always trigger their KeepAliveNotification handle when they are '
'deactivated, so that they (or their handle) do not send spurious events later '
'when they are no longer in the tree.'
);
}
return true;
});
_handles.remove(handle);
if (_handles.isEmpty) {
if (SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.persistentCallbacks.index) {
// Build/layout haven't started yet so let's just schedule this for
// the next frame.
setState(() { _keepingAlive = false; });
} else {
// We were probably notified by a descendant when they were yanked out
// of our subtree somehow. We're probably in the middle of build or
// layout, so there's really nothing we can do to clean up this mess
// short of just scheduling another build to do the cleanup. This is
// very unfortunate, and means (for instance) that garbage collection
// of these resources won't happen for another 16ms.
//
// The problem is there's really no way for us to distinguish these
// cases:
//
// * We haven't built yet (or missed out chance to build), but
// someone above us notified our descendant and our descendant is
// disconnecting from us. If we could mark ourselves dirty we would
// be able to clean everything this frame. (This is a pretty
// unlikely scenario in practice. Usually things change before
// build/layout, not during build/layout.)
//
// * Our child changed, and as our old child went away, it notified
// us. We can't setState, since we _just_ built. We can't apply the
// parent data information to our child because we don't _have_ a
// child at this instant. We really want to be able to change our
// mind about how we built, so we can give the KeepAlive widget a
// new value, but it's too late.
//
// * A deep descendant in another build scope just got yanked, and in
// the process notified us. We could apply new parent data
// information, but it may or may not get applied this frame,
// depending on whether said child is in the same layout scope.
//
// * A descendant is being moved from one position under us to
// another position under us. They just notified us of the removal,
// at some point in the future they will notify us of the addition.
// We don't want to do anything. (This is why we check that
// _handles is still empty below.)
//
// * We're being notified in the paint phase, or even in a post-frame
// callback. Either way it is far too late for us to make our
// parent lay out again this frame, so the garbage won't get
// collected this frame.
//
// * We are being torn out of the tree ourselves, as is our
// descendant, and it notified us while it was being deactivated.
// We don't need to do anything, but we don't know yet because we
// haven't been deactivated yet. (This is why we check mounted
// below before calling setState.)
//
// Long story short, we have to schedule a new frame and request a
// frame there, but this is generally a bad practice, and you should
// avoid it if possible.
_keepingAlive = false;
scheduleMicrotask(() {
if (mounted && _handles.isEmpty) {
// If mounted is false, we went away as well, so there's nothing to do.
// If _handles is no longer empty, then another client (or the same
// client in a new place) registered itself before we had a chance to
// turn off keep-alive, so again there's nothing to do.
setState(() {
assert(!_keepingAlive);
});
}
});
}
}
};
}
@override
Widget build(BuildContext context) {
assert(_child != null);
return new KeepAlive(
keepAlive: _keepingAlive,
child: _child,
);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (_keepingAlive)
description.add('keeping subtree alive');
if (_handles == null) {
description.add('no notifications ever received');
} else {
description.add('${_handles.length} active client${ _handles.length == 1 ? "" : "s" }');
}
}
}
/// Indicates that the subtree through which this notification bubbles must be
/// kept alive even if it would normally be discarded as an optimisation.
///
/// For example, a focused text field might fire this notification to indicate
/// that it should not be disposed even if the user scrolls the field off
/// screen.
///
/// Each [KeepAliveNotification] is configured with a [handle] that consists of
/// a [Listenable] that is triggered when the subtree no longer needs to be kept
/// alive.
///
/// The [handle] should be triggered any time the sending widget is removed from
/// the tree (in [State.deactivate]). If the widget is then rebuilt and still
/// needs to be kept alive, it should immediately send a new notification
/// (possible with the very same [Listenable]) during build.
///
/// This notification is listened to by the [AutomaticKeepAlive] widget, which
/// is added to the tree automatically by [SliverList] (and [ListView]) and
/// [SliverGrid] (and [GridView]) widgets.
///
/// Failure to trigger the [handle] in the manner described above will likely
/// cause the [AutomaticKeepAlive] to lose track of whether the widget should be
/// kept alive or not, leading to memory leaks or lost data. For example, if the
/// widget that requested keep-alive is removed from the subtree but doesn't
/// trigger its [Listenable] on the way out, then the subtree will continue to
/// be kept alive until the list itself is disposed. Similarly, if the
/// [Listenable] is triggered while the widget needs to be kept alive, but a new
/// [KeepAliveNotification] is not immediately sent, then the widget risks being
/// garbage collected while it wants to be kept alive.
///
/// It is an error to use the same [handle] in two [KeepAliveNotification]s
/// within the same [AutomaticKeepAlive] without triggering that [handle] before
/// the second notification is sent.
///
/// For a more convenient way to interact with [AutomaticKeepAlive] widgets,
/// consider using [AutomaticKeepAliveClientMixin], which uses
/// [KeepAliveNotification] internally.
class KeepAliveNotification extends Notification {
/// Creates a notification to indicate that a subtree must be kept alive.
///
/// The [handle] must not be null.
const KeepAliveNotification(this.handle) : assert(handle != null);
/// A [Listenable] that will inform its clients when the widget that fired the
/// notification no longer needs to be kept alive.
///
/// The [Listenable] should be triggered any time the sending widget is
/// removed from the tree (in [State.deactivate]). If the widget is then
/// rebuilt and still needs to be kept alive, it should immediately send a new
/// notification (possible with the very same [Listenable]) during build.
///
/// See also:
///
/// * [KeepAliveHandle], a convenience class for use with this property.
final Listenable handle;
}
/// A [Listenable] which can be manually triggered.
///
/// Used with [KeepAliveNotification] objects as their
/// [KeepAliveNotification.handle].
///
/// For a more convenient way to interact with [AutomaticKeepAlive] widgets,
/// consider using [AutomaticKeepAliveClientMixin], which uses a
/// [KeepAliveHandle] internally.
class KeepAliveHandle extends ChangeNotifier {
/// Trigger the listeners to indicate that the widget
/// no longer needs to be kept alive.
void release() {
notifyListeners();
}
}
/// A mixin with convenience methods for clients of [AutomaticKeepAlive].
///
/// Subclasses must implement [wantKeepAlive], and their [build] methods must
/// call `super.build` (which will always return null).
///
/// Then, whenever [wantKeepAlive]'s value changes (or might change), the
/// subclass should call [updateKeepAlive].
///
/// See also:
///
/// * [AutomaticKeepAlive], which listens to messages from this mixin.
/// * [KeepAliveNotification], the notifications sent by this mixin.
abstract class AutomaticKeepAliveClientMixin extends State<StatefulWidget> {
// This class is intended to be used as a mixin, and should not be
// extended directly.
factory AutomaticKeepAliveClientMixin._() => null;
KeepAliveHandle _keepAliveHandle;
void _ensureKeepAlive() {
assert(_keepAliveHandle == null);
_keepAliveHandle = new KeepAliveHandle();
new KeepAliveNotification(_keepAliveHandle).dispatch(context);
}
void _releaseKeepAlive() {
_keepAliveHandle.release();
_keepAliveHandle = null;
}
/// Whether the current instance should be kept alive.
///
/// Call [updateKeepAlive] whenever this getter's value changes.
@protected
bool get wantKeepAlive;
/// Ensures that any [AutomaticKeepAlive] ancestors are in a good state, by
/// firing a [KeepAliveNotification] or triggering the [KeepAliveHandle] as
/// appropriate.
@protected
void updateKeepAlive() {
if (wantKeepAlive) {
if (_keepAliveHandle == null)
_ensureKeepAlive();
} else {
if (_keepAliveHandle != null)
_releaseKeepAlive();
}
}
@override
void initState() {
super.initState();
if (wantKeepAlive)
_ensureKeepAlive();
}
@override
void deactivate() {
if (_keepAliveHandle != null)
_releaseKeepAlive();
super.deactivate();
}
@mustCallSuper
@override
Widget build(BuildContext context) {
if (wantKeepAlive && _keepAliveHandle == null)
_ensureKeepAlive();
return null;
}
}
\ No newline at end of file
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
...@@ -164,7 +165,7 @@ class _DismissibleClipper extends CustomClipper<Rect> { ...@@ -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 @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -183,6 +184,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -183,6 +184,9 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
bool _dragUnderway = false; bool _dragUnderway = false;
Size _sizePriorToCollapse; Size _sizePriorToCollapse;
@override
bool get wantKeepAlive => _moveController?.isAnimating == true || _resizeController?.isAnimating == true;
@override @override
void dispose() { void dispose() {
_moveController.dispose(); _moveController.dispose();
...@@ -323,6 +327,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -323,6 +327,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
void _handleDismissStatusChanged(AnimationStatus status) { void _handleDismissStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.completed && !_dragUnderway) if (status == AnimationStatus.completed && !_dragUnderway)
_startResizeAnimation(); _startResizeAnimation();
updateKeepAlive();
} }
void _startResizeAnimation() { void _startResizeAnimation() {
...@@ -335,7 +340,8 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -335,7 +340,8 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
widget.onDismissed(_dismissDirection); widget.onDismissed(_dismissDirection);
} else { } else {
_resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this) _resizeController = new AnimationController(duration: widget.resizeDuration, vsync: this)
..addListener(_handleResizeProgressChanged); ..addListener(_handleResizeProgressChanged)
..addStatusListener((AnimationStatus status) => updateKeepAlive());
_resizeController.forward(); _resizeController.forward();
setState(() { setState(() {
_sizePriorToCollapse = context.size; _sizePriorToCollapse = context.size;
...@@ -362,6 +368,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin ...@@ -362,6 +368,7 @@ class _DismissibleState extends State<Dismissible> with TickerProviderStateMixin
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); // See AutomaticKeepAliveClientMixin.
Widget background = widget.background; Widget background = widget.background;
if (widget.secondaryBackground != null) { if (widget.secondaryBackground != null) {
final DismissDirection direction = _dismissDirection; final DismissDirection direction = _dismissDirection;
......
...@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; ...@@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'focus_scope.dart'; import 'focus_scope.dart';
...@@ -252,7 +253,7 @@ class EditableText extends StatefulWidget { ...@@ -252,7 +253,7 @@ class EditableText extends StatefulWidget {
description.add('focusNode: $focusNode'); description.add('focusNode: $focusNode');
if (obscureText != false) if (obscureText != false)
description.add('obscureText: $obscureText'); description.add('obscureText: $obscureText');
description.add('$style'); description.add('${style.toString().split("\n").join(", ")}');
if (textAlign != null) if (textAlign != null)
description.add('$textAlign'); description.add('$textAlign');
if (textScaleFactor != null) if (textScaleFactor != null)
...@@ -267,7 +268,7 @@ class EditableText extends StatefulWidget { ...@@ -267,7 +268,7 @@ class EditableText extends StatefulWidget {
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> implements TextInputClient { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient {
Timer _cursorTimer; Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false); final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
...@@ -278,6 +279,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -278,6 +279,9 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
final LayerLink _layerLink = new LayerLink(); final LayerLink _layerLink = new LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
// State lifecycle: // State lifecycle:
@override @override
...@@ -308,6 +312,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -308,6 +312,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
if (widget.focusNode != oldWidget.focusNode) { if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged); oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
} }
} }
...@@ -549,11 +554,13 @@ class EditableTextState extends State<EditableText> implements TextInputClient { ...@@ -549,11 +554,13 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
// Clear the selection and composition state if this widget lost focus. // Clear the selection and composition state if this widget lost focus.
_value = new TextEditingValue(text: _value.text); _value = new TextEditingValue(text: _value.text);
} }
updateKeepAlive();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(widget.focusNode); FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin.
return new Scrollable( return new Scrollable(
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right, axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController, controller: _scrollController,
......
...@@ -343,6 +343,22 @@ class LabeledGlobalKey<T extends State<StatefulWidget>> extends GlobalKey<T> { ...@@ -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 /// Used to tie the identity of a widget to the identity of an object used to
/// generate that widget. /// 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. /// Any [GlobalObjectKey] created for the same value will match.
@optionalTypeArgs @optionalTypeArgs
class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> { class GlobalObjectKey<T extends State<StatefulWidget>> extends GlobalKey<T> {
...@@ -1418,6 +1434,19 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge ...@@ -1418,6 +1434,19 @@ abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidge
/// parent, as appropriate. /// parent, as appropriate.
@protected @protected
void applyParentData(RenderObject renderObject); 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. /// Base class for widgets that efficiently propagate information down the tree.
...@@ -2470,6 +2499,11 @@ abstract class Element implements BuildContext { ...@@ -2470,6 +2499,11 @@ abstract class Element implements BuildContext {
/// ///
/// There is no guaranteed order in which the children will be visited, though /// There is no guaranteed order in which the children will be visited, though
/// it should be consistent over time. /// 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) { } void visitChildren(ElementVisitor visitor) { }
/// Calls the argument for each child that is relevant for semantics. By /// Calls the argument for each child that is relevant for semantics. By
...@@ -2477,11 +2511,20 @@ abstract class Element implements BuildContext { ...@@ -2477,11 +2511,20 @@ abstract class Element implements BuildContext {
/// to hide their children. /// to hide their children.
void visitChildrenForSemantics(ElementVisitor visitor) => visitChildren(visitor); void visitChildrenForSemantics(ElementVisitor visitor) => visitChildren(visitor);
/// Wrapper around visitChildren for BuildContext. /// Wrapper around [visitChildren] for [BuildContext].
@override @override
void visitChildElements(ElementVisitor visitor) { void visitChildElements(ElementVisitor visitor) {
// don't allow visitChildElements() during build, since children aren't necessarily built yet assert(() {
assert(owner == null || !owner._debugStateLocked); 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); visitChildren(visitor);
} }
...@@ -3685,18 +3728,60 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement { ...@@ -3685,18 +3728,60 @@ class ParentDataElement<T extends RenderObjectWidget> extends ProxyElement {
super.mount(parent, slot); super.mount(parent, slot);
} }
void _notifyChildren(Element child) { void _applyParentData(ParentDataWidget<T> widget) {
if (child is RenderObjectElement) { void applyParentDataToChild(Element child) {
child._updateParentData(widget); if (child is RenderObjectElement) {
} else { child._updateParentData(widget);
assert(child is! ParentDataElement<RenderObjectWidget>); } else {
child.visitChildren(_notifyChildren); 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 @override
void notifyClients(ParentDataWidget<T> oldWidget) { void notifyClients(ParentDataWidget<T> oldWidget) {
visitChildren(_notifyChildren); _applyParentData(widget);
} }
} }
......
...@@ -26,6 +26,8 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification ...@@ -26,6 +26,8 @@ typedef bool NotificationListenerCallback<T extends Notification>(T notification
/// widgets with the appropriate type parameters that are ancestors of the given /// widgets with the appropriate type parameters that are ancestors of the given
/// [BuildContext]. /// [BuildContext].
abstract class Notification { abstract class Notification {
const Notification();
/// Applied to each ancestor of the [dispatch] target. /// Applied to each ancestor of the [dispatch] target.
/// ///
/// The [Notification] class implementation of this method dispatches the /// The [Notification] class implementation of this method dispatches the
...@@ -89,7 +91,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget { ...@@ -89,7 +91,7 @@ class NotificationListener<T extends Notification> extends StatelessWidget {
const NotificationListener({ const NotificationListener({
Key key, Key key,
@required this.child, @required this.child,
this.onNotification this.onNotification,
}) : super(key: key); }) : super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
......
...@@ -515,8 +515,10 @@ class ListView extends BoxScrollView { ...@@ -515,8 +515,10 @@ class ListView extends BoxScrollView {
/// It is usually more efficient to create children on demand using [new /// It is usually more efficient to create children on demand using [new
/// ListView.builder]. /// ListView.builder].
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null. /// null.
ListView({ ListView({
Key key, Key key,
...@@ -528,10 +530,12 @@ class ListView extends BoxScrollView { ...@@ -528,10 +530,12 @@ class ListView extends BoxScrollView {
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
this.itemExtent, this.itemExtent,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate( }) : childrenDelegate = new SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), super( ), super(
key: key, key: key,
...@@ -563,9 +567,11 @@ class ListView extends BoxScrollView { ...@@ -563,9 +567,11 @@ class ListView extends BoxScrollView {
/// Even more efficient, however, is to create the instances on demand using /// Even more efficient, however, is to create the instances on demand using
/// this constructor's `itemBuilder` callback. /// this constructor's `itemBuilder` callback.
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
/// null. /// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property. Both must not
/// be null.
ListView.builder({ ListView.builder({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -578,10 +584,12 @@ class ListView extends BoxScrollView { ...@@ -578,10 +584,12 @@ class ListView extends BoxScrollView {
this.itemExtent, this.itemExtent,
@required IndexedWidgetBuilder itemBuilder, @required IndexedWidgetBuilder itemBuilder,
int itemCount, int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
}) : childrenDelegate = new SliverChildBuilderDelegate( }) : childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
childCount: itemCount, childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), super( ), super(
key: key, key: key,
...@@ -783,8 +791,10 @@ class GridView extends BoxScrollView { ...@@ -783,8 +791,10 @@ class GridView extends BoxScrollView {
/// ///
/// The [gridDelegate] argument must not be null. /// The [gridDelegate] argument must not be null.
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null. /// null.
GridView({ GridView({
Key key, Key key,
...@@ -796,11 +806,13 @@ class GridView extends BoxScrollView { ...@@ -796,11 +806,13 @@ class GridView extends BoxScrollView {
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
@required this.gridDelegate, @required this.gridDelegate,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = new SliverChildListDelegate( childrenDelegate = new SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), ),
super( super(
...@@ -828,9 +840,11 @@ class GridView extends BoxScrollView { ...@@ -828,9 +840,11 @@ class GridView extends BoxScrollView {
/// ///
/// The [gridDelegate] argument must not be null. /// The [gridDelegate] argument must not be null.
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
/// null. /// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property. Both must not
/// be null.
GridView.builder({ GridView.builder({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -843,11 +857,13 @@ class GridView extends BoxScrollView { ...@@ -843,11 +857,13 @@ class GridView extends BoxScrollView {
@required this.gridDelegate, @required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder, @required IndexedWidgetBuilder itemBuilder,
int itemCount, int itemCount,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = new SliverChildBuilderDelegate( childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
childCount: itemCount, childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), ),
super( super(
...@@ -897,8 +913,10 @@ class GridView extends BoxScrollView { ...@@ -897,8 +913,10 @@ class GridView extends BoxScrollView {
/// ///
/// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate]. /// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate].
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null. /// null.
/// ///
/// See also: /// See also:
...@@ -917,6 +935,7 @@ class GridView extends BoxScrollView { ...@@ -917,6 +935,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0, double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0, double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0, double childAspectRatio: 1.0,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount( }) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
...@@ -927,6 +946,7 @@ class GridView extends BoxScrollView { ...@@ -927,6 +946,7 @@ class GridView extends BoxScrollView {
), ),
childrenDelegate = new SliverChildListDelegate( childrenDelegate = new SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), super( ), super(
key: key, key: key,
...@@ -944,8 +964,10 @@ class GridView extends BoxScrollView { ...@@ -944,8 +964,10 @@ class GridView extends BoxScrollView {
/// ///
/// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate]. /// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate].
/// ///
/// The `addRepaintBoundaries` argument corresponds to the /// The `addAutomaticKeepAlives` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The
/// `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be
/// null. /// null.
/// ///
/// See also: /// See also:
...@@ -964,6 +986,7 @@ class GridView extends BoxScrollView { ...@@ -964,6 +986,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0, double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0, double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0, double childAspectRatio: 1.0,
bool addAutomaticKeepAlives: true,
bool addRepaintBoundaries: true, bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent( }) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
...@@ -974,6 +997,7 @@ class GridView extends BoxScrollView { ...@@ -974,6 +997,7 @@ class GridView extends BoxScrollView {
), ),
childrenDelegate = new SliverChildListDelegate( childrenDelegate = new SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
), super( ), super(
key: key, key: key,
......
...@@ -7,6 +7,7 @@ import 'dart:collection' show SplayTreeMap, HashMap; ...@@ -7,6 +7,7 @@ import 'dart:collection' show SplayTreeMap, HashMap;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -43,7 +44,7 @@ abstract class SliverChildDelegate { ...@@ -43,7 +44,7 @@ abstract class SliverChildDelegate {
/// exists. /// exists.
/// ///
/// Subclasses typically override this function and wrap their children in /// Subclasses typically override this function and wrap their children in
/// [RepaintBoundary] widgets. /// [AutomaticKeepAlive] and [RepaintBoundary] widgets.
Widget build(BuildContext context, int index); Widget build(BuildContext context, int index);
/// Returns an estimate of the number of children this delegate will build. /// Returns an estimate of the number of children this delegate will build.
...@@ -119,7 +120,9 @@ abstract class SliverChildDelegate { ...@@ -119,7 +120,9 @@ abstract class SliverChildDelegate {
/// not even have to be built until they are displayed. /// not even have to be built until they are displayed.
/// ///
/// The widgets returned from the builder callback are automatically wrapped in /// 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: /// See also:
/// ///
...@@ -129,12 +132,15 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -129,12 +132,15 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given /// Creates a delegate that supplies children for slivers using the given
/// builder callback. /// builder callback.
/// ///
/// The [builder] and [addRepaintBoundaries] arguments must not be null. /// The [builder], [addAutomaticKeepAlives], and [addRepaintBoundaries]
/// arguments must not be null.
const SliverChildBuilderDelegate( const SliverChildBuilderDelegate(
this.builder, { this.builder, {
this.childCount, this.childCount,
this.addAutomaticKeepAlives: true,
this.addRepaintBoundaries: true, this.addRepaintBoundaries: true,
}) : assert(builder != null), }) : assert(builder != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != null); assert(addRepaintBoundaries != null);
/// Called to build children for the sliver. /// Called to build children for the sliver.
...@@ -155,6 +161,20 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -155,6 +161,20 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// [builder] returns null. /// [builder] returns null.
final int childCount; 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]. /// Whether to wrap each child in a [RepaintBoundary].
/// ///
/// Typically, children in a scrolling container are wrapped in repaint /// Typically, children in a scrolling container are wrapped in repaint
...@@ -171,10 +191,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -171,10 +191,14 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
assert(builder != null); assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount)) if (index < 0 || (childCount != null && index >= childCount))
return null; return null;
final Widget child = builder(context, index); Widget child = builder(context, index);
if (child == null) if (child == null)
return 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 @override
...@@ -205,7 +229,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -205,7 +229,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// conditions. /// conditions.
/// ///
/// The widgets in the given [children] list are automatically wrapped in /// 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: /// See also:
/// ///
...@@ -215,13 +241,30 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -215,13 +241,30 @@ class SliverChildListDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given /// Creates a delegate that supplies children for slivers using the given
/// list. /// list.
/// ///
/// The [children] and [addRepaintBoundaries] arguments must not be null. /// The [children], [addAutomaticKeepAlives], and [addRepaintBoundaries]
/// arguments must not be null.
const SliverChildListDelegate( const SliverChildListDelegate(
this.children, { this.children, {
this.addAutomaticKeepAlives: true,
this.addRepaintBoundaries: true, this.addRepaintBoundaries: true,
}) : assert(children != null), }) : assert(children != null),
assert(addAutomaticKeepAlives != null),
assert(addRepaintBoundaries != 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]. /// Whether to wrap each child in a [RepaintBoundary].
/// ///
/// Typically, children in a scrolling container are wrapped in repaint /// Typically, children in a scrolling container are wrapped in repaint
...@@ -241,9 +284,13 @@ class SliverChildListDelegate extends SliverChildDelegate { ...@@ -241,9 +284,13 @@ class SliverChildListDelegate extends SliverChildDelegate {
assert(children != null); assert(children != null);
if (index < 0 || index >= children.length) if (index < 0 || index >= children.length)
return null; return null;
final Widget child = children[index]; Widget child = children[index];
assert(child != null); 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 @override
...@@ -875,11 +922,18 @@ class KeepAlive extends ParentDataWidget<SliverMultiBoxAdaptorWidget> { ...@@ -875,11 +922,18 @@ class KeepAlive extends ParentDataWidget<SliverMultiBoxAdaptorWidget> {
if (parentData.keepAlive != keepAlive) { if (parentData.keepAlive != keepAlive) {
parentData.keepAlive = keepAlive; parentData.keepAlive = keepAlive;
final AbstractNode targetParent = renderObject.parent; final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject) if (targetParent is RenderObject && !keepAlive)
targetParent.markNeedsLayout(); 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 @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
......
...@@ -19,6 +19,7 @@ export 'src/widgets/animated_list.dart'; ...@@ -19,6 +19,7 @@ export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart'; export 'src/widgets/animated_size.dart';
export 'src/widgets/app.dart'; export 'src/widgets/app.dart';
export 'src/widgets/async.dart'; export 'src/widgets/async.dart';
export 'src/widgets/automatic_keep_alive.dart';
export 'src/widgets/banner.dart'; export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart'; export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
......
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
import 'feedback_tester.dart'; import 'feedback_tester.dart';
void main() { void main() {
...@@ -116,4 +118,37 @@ void main() { ...@@ -116,4 +118,37 @@ void main() {
expect(feedback.hapticCount, 0); 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() { ...@@ -132,4 +132,78 @@ void main() {
expect(tester.testTextInput.isVisible, isFalse); 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'; ...@@ -36,8 +36,13 @@ import 'recording_canvas.dart';
/// order). /// order).
/// ///
/// See [PaintPattern] for a discussion of the semantics of paint patterns. /// See [PaintPattern] for a discussion of the semantics of paint patterns.
///
/// To match something which paints nothing, see [paintsNothing].
PaintPattern get paints => new _TestRecordingCanvasPatternMatcher(); 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. /// Signature for [PaintPattern.something] predicate argument.
/// ///
/// Used by the [paints] matcher. /// Used by the [paints] matcher.
...@@ -243,7 +248,73 @@ abstract class PaintPattern { ...@@ -243,7 +248,73 @@ abstract class PaintPattern {
void something(PaintPatternPredicate predicate); 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>[]; final List<_PaintPredicate> _predicates = <_PaintPredicate>[];
@override @override
...@@ -326,43 +397,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern ...@@ -326,43 +397,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
_predicates.add(new _SomethingPaintPredicate(predicate)); _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 @override
Description describe(Description description) { Description describe(Description description) {
if (_predicates.isEmpty) if (_predicates.isEmpty)
...@@ -375,18 +409,11 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern ...@@ -375,18 +409,11 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
} }
@override @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) { bool _evaluatePredicates(Iterable<Invocation> calls, StringBuffer description) {
// If we ever want to have a matcher for painting nothing, create a separate if (calls.isEmpty) {
// paintsNothing matcher. description.write('painted nothing.');
return false;
}
if (_predicates.isEmpty) { if (_predicates.isEmpty) {
description.write( description.write(
'painted something, but you must now add a pattern to the paints matcher ' 'painted something, but you must now add a pattern to the paints matcher '
...@@ -394,10 +421,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern ...@@ -394,10 +421,6 @@ class _TestRecordingCanvasPatternMatcher extends Matcher implements PaintPattern
); );
return false; return false;
} }
if (calls.isEmpty) {
description.write('painted nothing.');
return false;
}
final Iterator<_PaintPredicate> predicate = _predicates.iterator; final Iterator<_PaintPredicate> predicate = _predicates.iterator;
final Iterator<Invocation> call = calls.iterator..moveNext(); final Iterator<Invocation> call = calls.iterator..moveNext();
try { try {
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class Leaf extends StatefulWidget {
Leaf({ Key key, this.child }) : super(key: key);
final Widget child;
@override
_LeafState createState() => new _LeafState();
}
class _LeafState extends State<Leaf> {
bool _keepAlive = false;
KeepAliveHandle _handle;
@override
void deactivate() {
_handle?.release();
_handle = null;
super.deactivate();
}
void setKeepAlive(bool value) {
_keepAlive = value;
if (_keepAlive) {
if (_handle == null) {
_handle = new KeepAliveHandle();
new KeepAliveNotification(_handle).dispatch(context);
}
} else {
_handle?.release();
_handle = null;
}
}
@override
Widget build(BuildContext context) {
if (_keepAlive && _handle == null) {
_handle = new KeepAliveHandle();
new KeepAliveNotification(_handle).dispatch(context);
}
return widget.child;
}
}
List<Widget> generateList(Widget child, { @required bool impliedMode }) {
return new List<Widget>.generate(
100,
(int index) {
final Widget result = new Leaf(
key: new GlobalObjectKey<_LeafState>(index),
child: child,
);
if (impliedMode)
return result;
return new AutomaticKeepAlive(child: result);
},
growable: false,
);
}
void tests({ @required bool impliedMode }) {
testWidgets('AutomaticKeepAlive with ListView with itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder(), impliedMode: impliedMode),
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
testWidgets('AutomaticKeepAlive with ListView without itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
children: generateList(
new Container(height: 12.3, child: const Placeholder()), // about 50 widgets visible
impliedMode: impliedMode,
),
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
testWidgets('AutomaticKeepAlive with GridView', (WidgetTester tester) async {
await tester.pumpWidget(new GridView.count(
addAutomaticKeepAlives: impliedMode,
addRepaintBoundaries: impliedMode,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
children: generateList(
new Container(child: const Placeholder()),
impliedMode: impliedMode,
),
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(GridView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(GridView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
}
void main() {
group('Explicit automatic keep-alive', () { tests(impliedMode: false); });
group('Implied automatic keep-alive', () { tests(impliedMode: true); });
testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
children: <Widget>[
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
key: const GlobalObjectKey<_LeafState>(2),
height: 400.0,
),
),
new AutomaticKeepAlive(
child: new Container(
key: const GlobalObjectKey<_LeafState>(3),
height: 400.0,
),
),
],
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0)); // move to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
const GlobalObjectKey<_LeafState>(1).currentState.setKeepAlive(true);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
const GlobalObjectKey<_LeafState>(1).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
});
testWidgets('AutomaticKeepAlive double', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
children: <Widget>[
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()),
]),
),
),
],
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsNothing);
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); // move to bottom
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget);
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
children: <Widget>[
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()),
]),
),
),
],
));
await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up.
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
await tester.drag(find.byType(ListView), const Offset(0.0, 1000.0)); // move to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsOneWidget);
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
children: <Widget>[
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(1), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(2), child: const Placeholder()),
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
]),
),
),
new AutomaticKeepAlive(
child: new Container(
height: 400.0,
child: new Row(children: <Widget>[
new Leaf(key: const GlobalObjectKey<_LeafState>(3), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(4), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(5), child: const Placeholder()),
new Leaf(key: const GlobalObjectKey<_LeafState>(0), child: const Placeholder()),
]),
),
),
],
));
await tester.pump(); // Sometimes AutomaticKeepAlive needs an extra pump to clean things up.
expect(find.byKey(const GlobalObjectKey<_LeafState>(1)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(2)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(4)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(5)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(0)), findsNothing);
});
}
\ No newline at end of file
...@@ -8,8 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -8,8 +8,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class Leaf extends StatefulWidget { class Leaf extends StatefulWidget {
Leaf({ Key key, this.index, this.child }) : super(key: key); Leaf({ Key key, this.child }) : super(key: key);
final int index;
final Widget child; final Widget child;
@override @override
_LeafState createState() => new _LeafState(); _LeafState createState() => new _LeafState();
...@@ -36,7 +35,6 @@ List<Widget> generateList(Widget child) { ...@@ -36,7 +35,6 @@ List<Widget> generateList(Widget child) {
100, 100,
(int index) => new Leaf( (int index) => new Leaf(
key: new GlobalObjectKey<_LeafState>(index), key: new GlobalObjectKey<_LeafState>(index),
index: index,
child: child, child: child,
), ),
growable: false, growable: false,
...@@ -46,6 +44,7 @@ List<Widget> generateList(Widget child) { ...@@ -46,6 +44,7 @@ List<Widget> generateList(Widget child) {
void main() { void main() {
testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async { testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView( await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
itemExtent: 12.3, // about 50 widgets visible itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()), children: generateList(const Placeholder()),
...@@ -85,6 +84,7 @@ void main() { ...@@ -85,6 +84,7 @@ void main() {
testWidgets('KeepAlive with ListView without itemExtent', (WidgetTester tester) async { testWidgets('KeepAlive with ListView without itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView( await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
children: generateList(new Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible children: generateList(new Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
)); ));
...@@ -123,6 +123,7 @@ void main() { ...@@ -123,6 +123,7 @@ void main() {
testWidgets('KeepAlive with GridView', (WidgetTester tester) async { testWidgets('KeepAlive with GridView', (WidgetTester tester) async {
await tester.pumpWidget(new GridView.count( await tester.pumpWidget(new GridView.count(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
crossAxisCount: 2, crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
...@@ -163,6 +164,7 @@ void main() { ...@@ -163,6 +164,7 @@ void main() {
testWidgets('KeepAlive render tree description', (WidgetTester tester) async { testWidgets('KeepAlive render tree description', (WidgetTester tester) async {
await tester.pumpWidget(new ListView( await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
addRepaintBoundaries: false, addRepaintBoundaries: false,
itemExtent: 400.0, // 2 visible children itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()), children: generateList(const Placeholder()),
......
...@@ -260,6 +260,7 @@ void main() { ...@@ -260,6 +260,7 @@ void main() {
testWidgets('ListView underflow extents', (WidgetTester tester) async { testWidgets('ListView underflow extents', (WidgetTester tester) async {
await tester.pumpWidget(new ListView( await tester.pumpWidget(new ListView(
addAutomaticKeepAlives: false,
children: <Widget>[ children: <Widget>[
new Container(height: 100.0), new Container(height: 100.0),
new Container(height: 100.0), new Container(height: 100.0),
......
...@@ -15,7 +15,7 @@ void main() { ...@@ -15,7 +15,7 @@ void main() {
new CustomScrollView( new CustomScrollView(
slivers: <Widget>[ slivers: <Widget>[
new SliverFillViewport( 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