Keep-alive for widgets in lazy lists (#11010)
diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
index 4177742..2b3eefb 100644
--- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
+++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart
@@ -130,8 +130,9 @@
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
- if (leadingGarbage + trailingGarbage > 0)
- collectGarbage(leadingGarbage, trailingGarbage);
+ collectGarbage(leadingGarbage, trailingGarbage);
+ } else {
+ collectGarbage(0, 0);
}
if (firstChild == null) {
diff --git a/packages/flutter/lib/src/rendering/sliver_grid.dart b/packages/flutter/lib/src/rendering/sliver_grid.dart
index 2bebc23..7fb0cd5 100644
--- a/packages/flutter/lib/src/rendering/sliver_grid.dart
+++ b/packages/flutter/lib/src/rendering/sliver_grid.dart
@@ -507,8 +507,9 @@
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
- if (leadingGarbage + trailingGarbage > 0)
- collectGarbage(leadingGarbage, trailingGarbage);
+ collectGarbage(leadingGarbage, trailingGarbage);
+ } else {
+ collectGarbage(0, 0);
}
final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex);
diff --git a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart
index 09a791e..0dff9b9 100644
--- a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart
+++ b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart
@@ -111,8 +111,15 @@
/// The index of this child according to the [RenderSliverBoxChildManager].
int index;
+ /// Whether to keep the child alive even when it is no longer visible.
+ bool keepAlive = false;
+
+ /// Whether the widget is currently in the
+ /// [RenderSliverMultiBoxAdaptor._keepAliveBucket].
+ bool _keptAlive = false;
+
@override
- String toString() => 'index=$index; ${super.toString()}';
+ String toString() => 'index=$index; ${keepAlive == true ? "keepAlive; " : ""}${super.toString()}';
}
/// A sliver with multiple box children.
@@ -168,10 +175,15 @@
RenderSliverBoxChildManager get childManager => _childManager;
final RenderSliverBoxChildManager _childManager;
+ /// The nodes being kept alive despite not being visible.
+ final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
+
@override
void adoptChild(RenderObject child) {
super.adoptChild(child);
- childManager.didAdoptChild(child);
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ if (!childParentData._keptAlive)
+ childManager.didAdoptChild(child);
}
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
@@ -192,64 +204,139 @@
});
}
+ @override
+ void remove(RenderBox child) {
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ if (!childParentData._keptAlive) {
+ super.remove(child);
+ return;
+ }
+ assert(_keepAliveBucket[childParentData.index] == child);
+ _keepAliveBucket.remove(childParentData.index);
+ dropChild(child);
+ }
+
+ @override
+ void removeAll() {
+ super.removeAll();
+ for (RenderBox child in _keepAliveBucket.values)
+ dropChild(child);
+ _keepAliveBucket.clear();
+ }
+
+ void _createOrObtainChild(int index, { RenderBox after }) {
+ invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
+ assert(constraints == this.constraints);
+ if (_keepAliveBucket.containsKey(index)) {
+ final RenderBox child = _keepAliveBucket.remove(index);
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ assert(childParentData._keptAlive);
+ dropChild(child);
+ child.parentData = childParentData;
+ insert(child, after: after);
+ childParentData._keptAlive = false;
+ } else {
+ _childManager.createChild(index, after: after);
+ }
+ });
+ }
+
+ void _destroyOrCacheChild(RenderBox child) {
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ if (childParentData.keepAlive) {
+ assert(!childParentData._keptAlive);
+ remove(child);
+ _keepAliveBucket[childParentData.index] = child;
+ child.parentData = childParentData;
+ super.adoptChild(child);
+ childParentData._keptAlive = true;
+ } else {
+ assert(child.parent == this);
+ _childManager.removeChild(child);
+ assert(child.parent == null);
+ }
+ }
+
+ @override
+ void attach(PipelineOwner owner) {
+ super.attach(owner);
+ for (RenderBox child in _keepAliveBucket.values)
+ child.attach(owner);
+ }
+
+ @override
+ void detach() {
+ super.detach();
+ for (RenderBox child in _keepAliveBucket.values)
+ child.detach();
+ }
+
+ @override
+ void redepthChildren() {
+ super.redepthChildren();
+ for (RenderBox child in _keepAliveBucket.values)
+ redepthChild(child);
+ }
+
+ @override
+ void visitChildren(RenderObjectVisitor visitor) {
+ super.visitChildren(visitor);
+ for (RenderBox child in _keepAliveBucket.values)
+ visitor(child);
+ }
+
/// Called during layout to create and add the child with the given index and
/// scroll offset.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
- /// the child.
+ /// the child if necessary. The child may instead be obtained from a cache;
+ /// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
- /// Returns false if createChild did not add any child, otherwise returns
- /// true.
+ /// Returns false if there was no cached child and `createChild` did not add
+ /// any child, otherwise returns true.
///
/// Does not layout the new child.
///
- /// When this is called, there are no children, so no children can be removed
- /// during the call to createChild. No child should be added during that call
- /// either, except for the one that is created and returned by createChild.
+ /// When this is called, there are no visible children, so no children can be
+ /// removed during the call to `createChild`. No child should be added during
+ /// that call either, except for the one that is created and returned by
+ /// `createChild`.
@protected
bool addInitialChild({ int index: 0, double layoutOffset: 0.0 }) {
assert(_debugAssertChildListLocked());
assert(firstChild == null);
- bool result;
- invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
- assert(constraints == this.constraints);
- _childManager.createChild(index, after: null);
- if (firstChild != null) {
- assert(firstChild == lastChild);
- assert(indexOf(firstChild) == index);
- final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
- firstChildParentData.layoutOffset = layoutOffset;
- result = true;
- } else {
- childManager.setDidUnderflow(true);
- result = false;
- }
- });
- return result;
+ _createOrObtainChild(index, after: null);
+ if (firstChild != null) {
+ assert(firstChild == lastChild);
+ assert(indexOf(firstChild) == index);
+ final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
+ firstChildParentData.layoutOffset = layoutOffset;
+ return true;
+ }
+ childManager.setDidUnderflow(true);
+ return false;
}
/// Called during layout to create, add, and layout the child before
/// [firstChild].
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
- /// the child.
+ /// the child if necessary. The child may instead be obtained from a cache;
+ /// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
- /// Returns the new child or null if no child is created.
+ /// Returns the new child or null if no child was obtained.
///
/// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except
- /// for the one that is created and returned by createChild.
+ /// for the one that is created and returned by `createChild`.
@protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {
bool parentUsesSize: false,
}) {
assert(_debugAssertChildListLocked());
final int index = indexOf(firstChild) - 1;
- invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
- assert(constraints == this.constraints);
- _childManager.createChild(index, after: null);
- });
+ _createOrObtainChild(index, after: null);
if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: parentUsesSize);
return firstChild;
@@ -262,7 +349,8 @@
/// the given child.
///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
- /// the child.
+ /// the child if necessary. The child may instead be obtained from a cache;
+ /// see [SliverMultiBoxAdaptorParentData.keepAlive].
///
/// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset.
@@ -277,13 +365,9 @@
assert(_debugAssertChildListLocked());
assert(after != null);
final int index = indexOf(after) + 1;
- invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
- assert(constraints == this.constraints);
- _childManager.createChild(index, after: after);
- });
+ _createOrObtainChild(index, after: after);
final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) {
- assert(indexOf(child) == index);
child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child;
}
@@ -293,19 +377,37 @@
/// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list.
+ ///
+ /// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is
+ /// set to true will be removed to a cache instead of being dropped.
+ ///
+ /// This method also collects any children that were previously kept alive but
+ /// are now no longer necessary. As such, it should be called every time
+ /// [performLayout] is run, even if the arguments are both zero.
@protected
void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) {
- _childManager.removeChild(firstChild);
+ _destroyOrCacheChild(firstChild);
leadingGarbage -= 1;
}
while (trailingGarbage > 0) {
- _childManager.removeChild(lastChild);
+ _destroyOrCacheChild(lastChild);
trailingGarbage -= 1;
}
+ // Ask the child manager to remove the children that are no longer being
+ // kept alive. (This should cause _keepAliveBucket to change, so we have
+ // to prepare our list ahead of time.)
+ _keepAliveBucket.values.where((RenderBox child) {
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ return !childParentData.keepAlive;
+ }).toList().forEach(_childManager.removeChild);
+ assert(_keepAliveBucket.values.where((RenderBox child) {
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ return !childParentData.keepAlive;
+ }).isEmpty);
});
}
@@ -442,4 +544,42 @@
});
return true;
}
+
+ @override
+ String debugDescribeChildren(String prefix) {
+ StringBuffer result;
+ if (firstChild != null) {
+ result = new StringBuffer()
+ ..write(prefix)
+ ..write(' \u2502\n');
+ RenderBox child = firstChild;
+ while (child != lastChild) {
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u2502"));
+ child = childParentData.nextSibling;
+ }
+ if (child != null) {
+ assert(child == lastChild);
+ final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
+ if (_keepAliveBucket.isEmpty) {
+ result.write(child.toStringDeep("$prefix \u2514\u2500child with index ${childParentData.index}: ", "$prefix "));
+ } else {
+ result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u254E"));
+ }
+ }
+ }
+ if (_keepAliveBucket.isNotEmpty) {
+ result ??= new StringBuffer()
+ ..write(prefix)
+ ..write(' \u254E\n');
+ final List<int> indices = _keepAliveBucket.keys.toList()..sort();
+ final int lastIndex = indices.removeLast();
+ if (indices.isNotEmpty) {
+ for (int index in indices)
+ result.write(_keepAliveBucket[index].toStringDeep("$prefix \u251C\u2500child with index $index (kept alive offstage): ", "$prefix \u254E"));
+ }
+ result.write(_keepAliveBucket[lastIndex].toStringDeep("$prefix \u2514\u2500child with index $lastIndex (kept alive offstage): ", "$prefix "));
+ }
+ return result?.toString() ?? '';
+ }
}
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 372a845..77f6fa3 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -1201,8 +1201,9 @@
/// Meta data for identifying children in a [CustomMultiChildLayout].
///
-/// The [MultiChildLayoutDelegate] hasChild, layoutChild, and positionChild
-/// methods use these identifiers.
+/// The [MultiChildLayoutDelegate.hasChild],
+/// [MultiChildLayoutDelegate.layoutChild], and
+/// [MultiChildLayoutDelegate.positionChild] methods use these identifiers.
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
/// Marks a child with a layout identifier.
///
diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart
index 09b5a36..e482b75 100644
--- a/packages/flutter/lib/src/widgets/scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/scroll_view.dart
@@ -514,6 +514,10 @@
///
/// 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
+ /// null.
ListView({
Key key,
Axis scrollDirection: Axis.vertical,
@@ -524,8 +528,12 @@
bool shrinkWrap: false,
EdgeInsets padding,
this.itemExtent,
+ bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
- }) : childrenDelegate = new SliverChildListDelegate(children), super(
+ }) : childrenDelegate = new SliverChildListDelegate(
+ children,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
@@ -554,6 +562,10 @@
/// [ListView] itself is created, it is more efficient to use [new ListView].
/// 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.
ListView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
@@ -566,7 +578,12 @@
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
- }) : childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super(
+ bool addRepaintBoundaries: true,
+ }) : childrenDelegate = new SliverChildBuilderDelegate(
+ itemBuilder,
+ childCount: itemCount,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
@@ -765,6 +782,10 @@
/// [SliverGridDelegate].
///
/// The [gridDelegate] argument must not be null.
+ ///
+ /// The `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
+ /// null.
GridView({
Key key,
Axis scrollDirection: Axis.vertical,
@@ -775,9 +796,13 @@
bool shrinkWrap: false,
EdgeInsets padding,
@required this.gridDelegate,
+ bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : assert(gridDelegate != null),
- childrenDelegate = new SliverChildListDelegate(children),
+ childrenDelegate = new SliverChildListDelegate(
+ children,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ),
super(
key: key,
scrollDirection: scrollDirection,
@@ -802,6 +827,10 @@
/// zero and less than `itemCount`.
///
/// The [gridDelegate] argument must not be null.
+ ///
+ /// The `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
+ /// null.
GridView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
@@ -814,8 +843,13 @@
@required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
+ bool addRepaintBoundaries: true,
}) : assert(gridDelegate != null),
- childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
+ childrenDelegate = new SliverChildBuilderDelegate(
+ itemBuilder,
+ childCount: itemCount,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ),
super(
key: key,
scrollDirection: scrollDirection,
@@ -863,6 +897,10 @@
///
/// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate].
///
+ /// The `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
+ /// null.
+ ///
/// See also:
///
/// * [new SliverGrid.count], the equivalent constructor for [SliverGrid].
@@ -879,6 +917,7 @@
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
+ bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
@@ -886,7 +925,10 @@
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
- childrenDelegate = new SliverChildListDelegate(children), super(
+ childrenDelegate = new SliverChildListDelegate(
+ children,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
@@ -902,6 +944,10 @@
///
/// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate].
///
+ /// The `addRepaintBoundaries` argument corresponds to the
+ /// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
+ /// null.
+ ///
/// See also:
///
/// * [new SliverGrid.extent], the equivalent constructor for [SliverGrid].
@@ -918,6 +964,7 @@
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
+ bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
@@ -925,7 +972,10 @@
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
- childrenDelegate = new SliverChildListDelegate(children), super(
+ childrenDelegate = new SliverChildListDelegate(
+ children,
+ addRepaintBoundaries: addRepaintBoundaries,
+ ), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart
index e5ece91..98b76fb 100644
--- a/packages/flutter/lib/src/widgets/sliver.dart
+++ b/packages/flutter/lib/src/widgets/sliver.dart
@@ -115,8 +115,11 @@
///
/// Many slivers lazily construct their box children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides
-/// children using an [IndexedWidgetBuilder] callback. The widgets returned from
-/// the builder callback are wrapped in [RepaintBoundary] widgets.
+/// children using an [IndexedWidgetBuilder] callback, so that the children do
+/// 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).
///
/// See also:
///
@@ -124,8 +127,15 @@
/// of children.
class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
- /// builder callback
- const SliverChildBuilderDelegate(this.builder, { this.childCount });
+ /// builder callback.
+ ///
+ /// The [builder] and [addRepaintBoundaries] arguments must not be null.
+ const SliverChildBuilderDelegate(
+ this.builder, {
+ this.childCount,
+ this.addRepaintBoundaries: true,
+ }) : assert(builder != null),
+ assert(addRepaintBoundaries != null);
/// Called to build children for the sliver.
///
@@ -145,6 +155,17 @@
/// [builder] returns null.
final int childCount;
+ /// Whether to wrap each child in a [RepaintBoundary].
+ ///
+ /// Typically, children in a scrolling container are wrapped in repaint
+ /// boundaries so that they do not need to be repainted as the list scrolls.
+ /// If the children are easy to repaint (e.g., solid color blocks or a short
+ /// snippet of text), it might be more efficient to not add a repaint boundary
+ /// and simply repaint the children during scrolling.
+ ///
+ /// Defaults to true.
+ final bool addRepaintBoundaries;
+
@override
Widget build(BuildContext context, int index) {
assert(builder != null);
@@ -153,7 +174,7 @@
final Widget child = builder(context, index);
if (child == null)
return null;
- return new RepaintBoundary.wrap(child, index);
+ return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
}
@override
@@ -183,6 +204,9 @@
/// demand). For example, the body of a dialog box might fit both of these
/// conditions.
///
+/// The widgets in the given [children] list are automatically wrapped in
+/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
+///
/// See also:
///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
@@ -190,7 +214,13 @@
class SliverChildListDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// list.
- const SliverChildListDelegate(this.children, { this.addRepaintBoundaries: true });
+ ///
+ /// The [children] and [addRepaintBoundaries] arguments must not be null.
+ const SliverChildListDelegate(
+ this.children, {
+ this.addRepaintBoundaries: true,
+ }) : assert(children != null),
+ assert(addRepaintBoundaries != null);
/// Whether to wrap each child in a [RepaintBoundary].
///
@@ -815,3 +845,44 @@
@override
RenderSliverFillRemaining createRenderObject(BuildContext context) => new RenderSliverFillRemaining();
}
+
+/// Mark a child as needing to stay alive even when it's in a lazy list that
+/// would otherwise remove it.
+///
+/// This widget is for use in [SliverMultiBoxAdaptorWidget]s, such as
+/// [SliverGrid] or [SliverList].
+class KeepAlive extends ParentDataWidget<SliverMultiBoxAdaptorWidget> {
+ /// Marks a child as needing to remain alive.
+ ///
+ /// The [child] and [keepAlive] arguments must not be null.
+ KeepAlive({
+ Key key,
+ @required this.keepAlive,
+ @required Widget child,
+ }) : assert(child != null),
+ assert(keepAlive != null),
+ super(key: key, child: child);
+
+ /// Whether to keep the child alive.
+ ///
+ /// If this is false, it is as if this widget was omitted.
+ final bool keepAlive;
+
+ @override
+ void applyParentData(RenderObject renderObject) {
+ assert(renderObject.parentData is SliverMultiBoxAdaptorParentData);
+ final SliverMultiBoxAdaptorParentData parentData = renderObject.parentData;
+ if (parentData.keepAlive != keepAlive) {
+ parentData.keepAlive = keepAlive;
+ final AbstractNode targetParent = renderObject.parent;
+ if (targetParent is RenderObject)
+ targetParent.markNeedsLayout();
+ }
+ }
+
+ @override
+ void debugFillDescription(List<String> description) {
+ super.debugFillDescription(description);
+ description.add('keepAlive: $keepAlive');
+ }
+}
diff --git a/packages/flutter/test/rendering/slivers_block_test.dart b/packages/flutter/test/rendering/slivers_block_test.dart
index 7752617..aee0b65 100644
--- a/packages/flutter/test/rendering/slivers_block_test.dart
+++ b/packages/flutter/test/rendering/slivers_block_test.dart
@@ -244,4 +244,23 @@
expect(inner.geometry.scrollOffsetCorrection, isNull);
});
+
+ test('SliverMultiBoxAdaptorParentData.toString', () {
+ final SliverMultiBoxAdaptorParentData candidate = new SliverMultiBoxAdaptorParentData();
+ expect(candidate.keepAlive, isFalse);
+ expect(candidate.index, isNull);
+ expect(candidate.toString(), 'index=null; layoutOffset=0.0');
+ candidate.keepAlive = null;
+ expect(candidate.toString(), 'index=null; layoutOffset=0.0');
+ candidate.keepAlive = true;
+ expect(candidate.toString(), 'index=null; keepAlive; layoutOffset=0.0');
+ candidate.keepAlive = false;
+ expect(candidate.toString(), 'index=null; layoutOffset=0.0');
+ candidate.index = 0;
+ expect(candidate.toString(), 'index=0; layoutOffset=0.0');
+ candidate.index = 1;
+ expect(candidate.toString(), 'index=1; layoutOffset=0.0');
+ candidate.index = -1;
+ expect(candidate.toString(), 'index=-1; layoutOffset=0.0');
+ });
}
diff --git a/packages/flutter/test/widgets/keep_alive_test.dart b/packages/flutter/test/widgets/keep_alive_test.dart
new file mode 100644
index 0000000..c211c6d
--- /dev/null
+++ b/packages/flutter/test/widgets/keep_alive_test.dart
@@ -0,0 +1,561 @@
+// 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:io' show Platform;
+
+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;
+ final Widget child;
+ @override
+ _LeafState createState() => new _LeafState();
+}
+
+class _LeafState extends State<Leaf> {
+ bool _keepAlive = false;
+
+ void setKeepAlive(bool value) {
+ setState(() { _keepAlive = value; });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return new KeepAlive(
+ keepAlive: _keepAlive,
+ child: widget.child,
+ );
+ }
+}
+
+List<Widget> generateList(Widget child) {
+ return new List<Widget>.generate(
+ 100,
+ (int index) => new Leaf(
+ key: new GlobalObjectKey<_LeafState>(index),
+ index: index,
+ child: child,
+ ),
+ growable: false,
+ );
+}
+
+void main() {
+ testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async {
+ await tester.pumpWidget(new ListView(
+ addRepaintBoundaries: false,
+ itemExtent: 12.3, // about 50 widgets visible
+ children: generateList(const Placeholder()),
+ ));
+ 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('KeepAlive with ListView without itemExtent', (WidgetTester tester) async {
+ await tester.pumpWidget(new ListView(
+ addRepaintBoundaries: false,
+ children: generateList(new Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
+ ));
+ 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('KeepAlive with GridView', (WidgetTester tester) async {
+ await tester.pumpWidget(new GridView.count(
+ addRepaintBoundaries: false,
+ crossAxisCount: 2,
+ childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
+ children: generateList(new Container(child: const Placeholder())),
+ ));
+ 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);
+ });
+
+ testWidgets('KeepAlive render tree description', (WidgetTester tester) async {
+ await tester.pumpWidget(new ListView(
+ addRepaintBoundaries: false,
+ itemExtent: 400.0, // 2 visible children
+ children: generateList(const Placeholder()),
+ ));
+ // The important lines below are the ones marked with "<----"
+ expect(tester.binding.renderView.toStringDeep(), equalsIgnoringHashCodes(
+ 'RenderView#00000\n'
+ ' │ debug mode enabled - ${Platform.operatingSystem}\n'
+ ' │ window size: Size(2400.0, 1800.0) (in physical pixels)\n'
+ ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
+ ' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n'
+ ' │\n'
+ ' └─child: RenderRepaintBoundary#00000\n'
+ ' │ creator: RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none>\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
+ ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
+ ' │ repaints)\n'
+ ' │\n'
+ ' └─child: RenderCustomPaint#00000\n'
+ ' │ creator: CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │\n'
+ ' └─child: RenderRepaintBoundary#00000\n'
+ ' │ creator: RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
+ ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
+ ' │ repaints)\n'
+ ' │\n'
+ ' └─child: RenderSemanticsGestureHandler#00000\n'
+ ' │ creator: _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ semantic boundary\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ gestures: horizontal scroll, vertical scroll\n'
+ ' │\n'
+ ' └─child: RenderPointerListener#00000\n'
+ ' │ creator: Listener ← _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ behavior: opaque\n'
+ ' │ listeners: down\n'
+ ' │\n'
+ ' └─child: RenderIgnorePointer#00000\n'
+ ' │ creator: IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
+ ' │ _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ ignoring: false\n'
+ ' │ ignoringSemantics: implicitly false\n'
+ ' │\n'
+ ' └─child: RenderViewport#00000\n'
+ ' │ creator: Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ⋯\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ AxisDirection.down\n'
+ ' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
+ ' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
+ ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
+ ' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
+ ' │ anchor: 0.0\n'
+ ' │\n'
+ ' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
+ ' │ creator: SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← ⋯\n'
+ ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
+ ' │ constraints: SliverConstraints(AxisDirection.down,\n'
+ ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
+ ' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
+ ' │ viewportMainAxisExtent: 600.0)\n'
+ ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
+ ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, )\n'
+ ' │ currently live children: 0 to 1\n'
+ ' │\n'
+ ' ├─child with index 0: RenderLimitedBox#00000\n'
+ ' │ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ │ ←\n'
+ ' │ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ │ ← RepaintBoundary ← ⋯\n'
+ ' │ │ parentData: index=0; layoutOffset=0.0\n'
+ ' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ │ size: Size(800.0, 400.0)\n'
+ ' │ │ maxWidth: 400.0\n'
+ ' │ │ maxHeight: 400.0\n'
+ ' │ │\n'
+ ' │ └─child: RenderCustomPaint#00000\n'
+ ' │ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← ⋯\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ size: Size(800.0, 400.0)\n'
+ ' │\n'
+ ' └─child with index 1: RenderLimitedBox#00000\n' // <----- no dashed line starts here
+ ' │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← ⋯\n'
+ ' │ parentData: index=1; layoutOffset=400.0\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ size: Size(800.0, 400.0)\n'
+ ' │ maxWidth: 400.0\n'
+ ' │ maxHeight: 400.0\n'
+ ' │\n'
+ ' └─child: RenderCustomPaint#00000\n'
+ ' creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ←\n'
+ ' RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ← ⋯\n'
+ ' parentData: <none> (can use size)\n'
+ ' constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' size: Size(800.0, 400.0)\n'
+ '' // TODO(ianh): remove blank line
+ ));
+ const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(true);
+ await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
+ await tester.pump();
+ const GlobalObjectKey<_LeafState>(3).currentState.setKeepAlive(true);
+ await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
+ await tester.pump();
+ expect(tester.binding.renderView.toStringDeep(), equalsIgnoringHashCodes(
+ 'RenderView#00000\n'
+ ' │ debug mode enabled - ${Platform.operatingSystem}\n'
+ ' │ window size: Size(2400.0, 1800.0) (in physical pixels)\n'
+ ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
+ ' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n'
+ ' │\n'
+ ' └─child: RenderRepaintBoundary#00000\n'
+ ' │ creator: RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none>\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
+ ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
+ ' │ repaints)\n'
+ ' │\n'
+ ' └─child: RenderCustomPaint#00000\n'
+ ' │ creator: CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │\n'
+ ' └─child: RenderRepaintBoundary#00000\n'
+ ' │ creator: RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
+ ' │ diagnosis: insufficient data to draw conclusion (less than five\n'
+ ' │ repaints)\n'
+ ' │\n'
+ ' └─child: RenderSemanticsGestureHandler#00000\n'
+ ' │ creator: _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ semantic boundary\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ gestures: horizontal scroll, vertical scroll\n'
+ ' │\n'
+ ' └─child: RenderPointerListener#00000\n'
+ ' │ creator: Listener ← _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ behavior: opaque\n'
+ ' │ listeners: down\n'
+ ' │\n'
+ ' └─child: RenderIgnorePointer#00000\n'
+ ' │ creator: IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
+ ' │ _GestureSemantics ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ ignoring: false\n'
+ ' │ ignoringSemantics: implicitly false\n'
+ ' │\n'
+ ' └─child: RenderViewport#00000\n'
+ ' │ creator: Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← Scrollable ← ⋯\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
+ ' │ layer: OffsetLayer#00000\n'
+ ' │ size: Size(800.0, 600.0)\n'
+ ' │ AxisDirection.down\n'
+ ' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
+ ' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
+ ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
+ ' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
+ ' │ anchor: 0.0\n'
+ ' │\n'
+ ' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
+ ' │ creator: SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
+ ' │ NotificationListener<ScrollNotification> ←\n'
+ ' │ GlowingOverscrollIndicator ← ⋯\n'
+ ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
+ ' │ constraints: SliverConstraints(AxisDirection.down,\n'
+ ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
+ ' │ 2000.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
+ ' │ viewportMainAxisExtent: 600.0)\n'
+ ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
+ ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, )\n'
+ ' │ currently live children: 5 to 6\n'
+ ' │\n'
+ ' ├─child with index 5: RenderLimitedBox#00000\n' // <----- this is index 5, not 0
+ ' │ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ │ ←\n'
+ ' │ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ │ ← RepaintBoundary ← ⋯\n'
+ ' │ │ parentData: index=5; layoutOffset=2000.0\n'
+ ' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ │ size: Size(800.0, 400.0)\n'
+ ' │ │ maxWidth: 400.0\n'
+ ' │ │ maxHeight: 400.0\n'
+ ' │ │\n'
+ ' │ └─child: RenderCustomPaint#00000\n'
+ ' │ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← ⋯\n'
+ ' │ parentData: <none> (can use size)\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ size: Size(800.0, 400.0)\n'
+ ' │\n'
+ ' ├─child with index 6: RenderLimitedBox#00000\n'
+ ' ╎ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n' // <----- the line starts becoming dashed here
+ ' ╎ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' ╎ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' ╎ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ╎ │ ←\n'
+ ' ╎ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ╎ │ ← RepaintBoundary ← ⋯\n'
+ ' ╎ │ parentData: index=6; layoutOffset=2400.0\n'
+ ' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' ╎ │ size: Size(800.0, 400.0)\n'
+ ' ╎ │ maxWidth: 400.0\n'
+ ' ╎ │ maxHeight: 400.0\n'
+ ' ╎ │\n'
+ ' ╎ └─child: RenderCustomPaint#00000\n'
+ ' ╎ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' ╎ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' ╎ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' ╎ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ╎ ←\n'
+ ' ╎ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ╎ ← ⋯\n'
+ ' ╎ parentData: <none> (can use size)\n'
+ ' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' ╎ size: Size(800.0, 400.0)\n'
+ ' ╎\n'
+ ' ├─child with index 0 (kept alive offstage): RenderLimitedBox#00000\n' // <----- this one is index 0 and is marked as being offstage
+ ' ╎ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' ╎ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' ╎ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' ╎ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ╎ │ ←\n'
+ ' ╎ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ╎ │ ← RepaintBoundary ← ⋯\n'
+ ' ╎ │ parentData: index=0; keepAlive; layoutOffset=0.0\n'
+ ' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' ╎ │ size: Size(800.0, 400.0)\n'
+ ' ╎ │ maxWidth: 400.0\n'
+ ' ╎ │ maxHeight: 400.0\n'
+ ' ╎ │\n'
+ ' ╎ └─child: RenderCustomPaint#00000\n'
+ ' ╎ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' ╎ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' ╎ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' ╎ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ╎ ←\n'
+ ' ╎ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ╎ ← ⋯\n'
+ ' ╎ parentData: <none> (can use size)\n'
+ ' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' ╎ size: Size(800.0, 400.0)\n'
+ ' ╎\n' // <----- dashed line ends here
+ ' └─child with index 3 (kept alive offstage): RenderLimitedBox#00000\n'
+ ' │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' │ ←\n'
+ ' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' │ ← RepaintBoundary ← ⋯\n'
+ ' │ parentData: index=3; keepAlive; layoutOffset=1200.0\n'
+ ' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' │ size: Size(800.0, 400.0)\n'
+ ' │ maxWidth: 400.0\n'
+ ' │ maxHeight: 400.0\n'
+ ' │\n'
+ ' └─child: RenderCustomPaint#00000\n'
+ ' creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
+ ' Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
+ ' SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
+ ' IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
+ ' ←\n'
+ ' RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
+ ' ← ⋯\n'
+ ' parentData: <none> (can use size)\n'
+ ' constraints: BoxConstraints(w=800.0, h=400.0)\n'
+ ' size: Size(800.0, 400.0)\n'
+ '' // TODO(ianh): remove blank line
+ ));
+ });
+
+}
diff --git a/packages/flutter/test/widgets/list_view_viewporting_test.dart b/packages/flutter/test/widgets/list_view_viewporting_test.dart
index 4c8a997..05bd5c5 100644
--- a/packages/flutter/test/widgets/list_view_viewporting_test.dart
+++ b/packages/flutter/test/widgets/list_view_viewporting_test.dart
@@ -295,7 +295,7 @@
' │ maxPaintExtent: 300.0, )\n'
' │ currently live children: 0 to 2\n'
' │\n'
- ' ├─child 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
+ ' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<0>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n'
@@ -347,7 +347,7 @@
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
- ' ├─child 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
+ ' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<1>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n'
@@ -399,7 +399,7 @@
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
- ' └─child 3: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
+ ' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ creator: RepaintBoundary-[<2>] ← SliverList ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n'
diff --git a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
index 9b9b8db..dbd8c50 100644
--- a/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
+++ b/packages/flutter/test/widgets/sliver_fill_viewport_test.dart
@@ -80,7 +80,7 @@
' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true, )\n'
' │ currently live children: 0 to 0\n'
' │\n'
- ' └─child 1: RenderRepaintBoundary#00000\n'
+ ' └─child with index 0: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary-[<0>] ← SliverFillViewport ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n'