Unverified Commit 0c80ed6d authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Keep alive support for 2D scrolling (#131641)

Fixes https://github.com/flutter/flutter/issues/126297

This adds support for keep alive to the 2D scrolling foundation. The TwoDimensionalChildBuilderDelegate and TwoDimensionalChildListDelegate will both add automatic keep alives to their children, matching the convention from SliverChildDelegates. The TwoDimensionalViewportParentData now incorporates keep alive and which is managed by the RenderTwoDimensionalViewport.
parent 9156f6b5
......@@ -395,8 +395,8 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
/// 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
/// Typically, lazily laid out children are wrapped in [AutomaticKeepAlive]
/// widgets so that the 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
......@@ -863,7 +863,6 @@ Widget _createErrorWidget(Object exception, StackTrace stackTrace) {
return ErrorWidget.builder(details);
}
// TODO(Piinks): Come back and add keep alive support, https://github.com/flutter/flutter/issues/126297
/// A delegate that supplies children for scrolling in two dimensions.
///
/// A [TwoDimensionalScrollView] lazily constructs its box children to avoid
......@@ -929,10 +928,11 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
/// Creates a delegate that supplies children for a [TwoDimensionalScrollView]
/// using the given builder callback.
TwoDimensionalChildBuilderDelegate({
this.addRepaintBoundaries = true,
required this.builder,
int? maxXIndex,
int? maxYIndex,
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
}) : assert(maxYIndex == null || maxYIndex >= 0),
assert(maxXIndex == null || maxXIndex >= 0),
_maxYIndex = maxYIndex,
......@@ -1028,6 +1028,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
final bool addRepaintBoundaries;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
final bool addAutomaticKeepAlives;
@override
Widget? build(BuildContext context, ChildVicinity vicinity) {
// If we have exceeded explicit upper bounds, return null.
......@@ -1050,6 +1053,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
if (addRepaintBoundaries) {
child = RepaintBoundary(child: child);
}
if (addAutomaticKeepAlives) {
child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}
return child;
}
......@@ -1095,6 +1101,7 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
/// null.
TwoDimensionalChildListDelegate({
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
required this.children,
});
......@@ -1114,6 +1121,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
final bool addRepaintBoundaries;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
final bool addAutomaticKeepAlives;
@override
Widget? build(BuildContext context, ChildVicinity vicinity) {
// If we have exceeded explicit upper bounds, return null.
......@@ -1128,7 +1138,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
if (addRepaintBoundaries) {
child = RepaintBoundary(child: child);
}
if (addAutomaticKeepAlives) {
child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}
return child;
}
......
......@@ -391,7 +391,7 @@ class _TwoDimensionalViewportElement extends RenderObjectElement
/// RenderTwoDimensionalViewport override the paint method, the [paintOffset]
/// should be used to position the child in the viewport in order to account for
/// a reversed [AxisDirection] in one or both dimensions.
class TwoDimensionalViewportParentData extends ParentData {
class TwoDimensionalViewportParentData extends ParentData with KeepAliveParentDataMixin {
/// The offset at which to paint the child in the parent's coordinate system.
///
/// This [Offset] represents the top left corner of the child of the
......@@ -472,14 +472,18 @@ class TwoDimensionalViewportParentData extends ParentData {
/// position the children instead of [layoutOffset].
Offset? paintOffset;
@override
bool get keptAlive => keepAlive && !isVisible;
@override
String toString() {
return 'vicinity=$vicinity; '
'layoutOffset=$layoutOffset; '
'paintOffset=$paintOffset; '
'${_paintExtent == null
? 'not visible '
: '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent'}';
? 'not visible; '
: '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent; '}'
'${keepAlive ? "keepAlive; " : ""}';
}
}
......@@ -493,9 +497,7 @@ class TwoDimensionalViewportParentData extends ParentData {
///
/// Subclasses should not override [performLayout], as it handles housekeeping
/// on either side of the call to [layoutChildSequence].
// TODO(Piinks): Two follow up changes:
// - Keep alive https://github.com/flutter/flutter/issues/126297
// - ensureVisible https://github.com/flutter/flutter/issues/126299
// TODO(Piinks): ensureVisible https://github.com/flutter/flutter/issues/126299
abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport {
/// Initializes fields for subclasses.
///
......@@ -527,7 +529,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
_delegate = delegate,
_mainAxis = mainAxis,
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
_clipBehavior = clipBehavior;
_clipBehavior = clipBehavior {
assert(() {
_debugDanglingKeepAlives = <RenderBox>[];
return true;
}());
}
/// Which part of the content inside the viewport should be visible in the
/// horizontal axis.
......@@ -674,6 +681,16 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
}
final TwoDimensionalChildManager _childManager;
final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{};
/// Children that have been laid out (or re-used) during the course of
/// performLayout, used to update the keep alive bucket at the end of
/// performLayout.
final Map<ChildVicinity, RenderBox> _activeChildrenForLayoutPass = <ChildVicinity, RenderBox>{};
/// The nodes being kept alive despite not being visible.
final Map<ChildVicinity, RenderBox> _keepAliveBucket = <ChildVicinity, RenderBox>{};
late List<RenderBox> _debugDanglingKeepAlives;
bool _hasVisualOverflow = false;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
......@@ -683,7 +700,6 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
@override
bool get sizedByParent => true;
final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{};
// Keeps track of the upper and lower bounds of ChildVicinity indices when
// subclasses call buildOrObtainChildFor during layoutChildSequence. These
// values are used to sort children in accordance with the mainAxis for
......@@ -788,6 +804,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) {
child.attach(owner);
}
for (final RenderBox child in _keepAliveBucket.values) {
child.attach(owner);
}
}
@override
......@@ -799,6 +818,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) {
child.detach();
}
for (final RenderBox child in _keepAliveBucket.values) {
child.detach();
}
}
@override
......@@ -806,6 +828,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) {
child.redepthChildren();
}
_keepAliveBucket.values.forEach(redepthChild);
}
@override
......@@ -815,6 +838,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
visitor(child);
child = parentDataOf(child)._nextSibling;
}
_keepAliveBucket.values.forEach(visitor);
}
@override
......@@ -824,11 +848,14 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
RenderBox? child = _firstChild;
while (child != null) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
// TODO(Piinks): When ensure visible is supported, remove this isVisible
// condition.
if (childParentData.isVisible) {
visitor(child);
}
child = childParentData._nextSibling;
}
// Do not visit children in [_keepAliveBucket].
}
@override
......@@ -959,6 +986,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
void performLayout() {
_firstChild = null;
_lastChild = null;
_activeChildrenForLayoutPass.clear();
_childManager._startLayout();
// Subclass lays out children.
......@@ -967,15 +995,35 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
assert(_debugCheckContentDimensions());
_didResize = false;
_needsDelegateRebuild = false;
_cacheKeepAlives();
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._endLayout();
assert(_debugOrphans?.isEmpty ?? true);
assert(_debugDanglingKeepAlives.isEmpty);
// Ensure we are not keeping anything alive that should not be any longer.
assert(_keepAliveBucket.values.where((RenderBox child) {
return !parentDataOf(child).keepAlive;
}).isEmpty);
// Organize children in paint order and complete parent data after
// un-used children are disposed of by the childManager.
_reifyChildren();
});
}
void _cacheKeepAlives() {
final List<RenderBox> remainingChildren = _children.values.toSet().difference(
_activeChildrenForLayoutPass.values.toSet()
).toList();
for (final RenderBox child in remainingChildren) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (childParentData.keepAlive) {
_keepAliveBucket[childParentData.vicinity] = child;
// Let the child manager know we intend to keep this.
_childManager._reuseChild(childParentData.vicinity);
}
}
}
// Ensures all children have a layoutOffset, sets paintExtent & paintOffset,
// and arranges children in paint order.
void _reifyChildren() {
......@@ -1082,12 +1130,20 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
return true;
}
/// Returns the child for a given [ChildVicinity].
/// Returns the child for a given [ChildVicinity], should be called during
/// [layoutChildSequence] in order to instantiate or retrieve children.
///
/// This method will build the child if it has not been already, or will reuse
/// it if it already exists.
/// it if it already exists, whether it was part of the previous frame or kept
/// alive.
///
/// Children for the given [ChildVicinity] will be inserted into the active
/// children list, and so should be visible, or contained within the
/// [cacheExtent].
RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) {
assert(vicinity != ChildVicinity.invalid);
// This should only be called during layout.
assert(debugDoingThisLayout);
if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) {
// First child of this layout pass. Set leading and trailing trackers.
_leadingXIndex = vicinity.xIndex;
......@@ -1107,11 +1163,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
_leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!);
_trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!);
}
if (_needsDelegateRebuild || !_children.containsKey(vicinity)) {
if (_needsDelegateRebuild || (!_children.containsKey(vicinity) && !_keepAliveBucket.containsKey(vicinity))) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._buildChild(vicinity);
});
} else {
_keepAliveBucket.remove(vicinity);
_childManager._reuseChild(vicinity);
}
if (!_children.containsKey(vicinity)) {
......@@ -1122,6 +1179,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
assert(_children.containsKey(vicinity));
final RenderBox child = _children[vicinity]!;
_activeChildrenForLayoutPass[vicinity] = child;
parentDataOf(child).vicinity = vicinity;
return child;
}
......@@ -1304,23 +1362,59 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot]));
assert(!_keepAliveBucket.containsValue(child));
_children[slot] = child;
adoptChild(child);
}
void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) {
if (_children[from] == child) {
_children.remove(from);
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (!childParentData.keptAlive) {
if (_children[from] == child) {
_children.remove(from);
}
assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child));
_children[to] = child;
return;
}
// If the child in the bucket is not current child, that means someone has
// already moved and replaced current child, and we cannot remove this
// child.
if (_keepAliveBucket[childParentData.vicinity] == child) {
_keepAliveBucket.remove(childParentData.vicinity);
}
assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child));
_children[to] = child;
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
// If there is an existing child in the new slot, that mean that child
// will be moved to other index. In other cases, the existing child should
// have been removed by _removeChild. Thus, it is ok to overwrite it.
assert(() {
if (_keepAliveBucket.containsKey(childParentData.vicinity)) {
_debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.vicinity]!);
}
return true;
}());
_keepAliveBucket[childParentData.vicinity] = child;
}
void _removeChild(RenderBox child, ChildVicinity slot) {
if (_children[slot] == child) {
_children.remove(slot);
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (!childParentData.keptAlive) {
if (_children[slot] == child) {
_children.remove(slot);
}
assert(_debugTrackOrphans(noLongerOrphan: child));
dropChild(child);
return;
}
assert(_debugTrackOrphans(noLongerOrphan: child));
assert(_keepAliveBucket[childParentData.vicinity] == child);
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
_keepAliveBucket.remove(childParentData.vicinity);
dropChild(child);
}
......
......@@ -393,18 +393,18 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport {
final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate;
final int rowCount;
final int columnCount;
rowCount = listDelegate.children.length - 1;
columnCount = listDelegate.children[0].length - 1;
rowCount = listDelegate.children.length;
columnCount = listDelegate.children[0].length;
final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0);
final int leadingRow = math.max((verticalPixels / 200).floor(), 0);
final int trailingColumn = math.min(
((horizontalPixels + viewportDimension.width) / 200).ceil(),
columnCount,
columnCount - 1,
);
final int trailingRow = math.min(
((verticalPixels + viewportDimension.height) / 200).ceil(),
rowCount,
rowCount - 1,
);
double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels;
......@@ -420,7 +420,51 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport {
}
xLayoutOffset += 200;
}
verticalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.height);
horizontalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.width);
verticalOffset.applyContentDimensions(
0.0,
math.max(200 * rowCount - viewportDimension.height, 0.0),
);
horizontalOffset.applyContentDimensions(
0,
math.max(200 * columnCount - viewportDimension.width, 0.0),
);
}
}
class KeepAliveCheckBox extends StatefulWidget {
const KeepAliveCheckBox({ super.key });
@override
KeepAliveCheckBoxState createState() => KeepAliveCheckBoxState();
}
class KeepAliveCheckBoxState extends State<KeepAliveCheckBox> with AutomaticKeepAliveClientMixin {
bool checkValue = false;
@override
bool get wantKeepAlive => _wantKeepAlive;
bool _wantKeepAlive = false;
set wantKeepAlive(bool value) {
if (_wantKeepAlive != value) {
_wantKeepAlive = value;
updateKeepAlive();
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Checkbox(
value: checkValue,
onChanged: (bool? value) {
if (checkValue != value) {
setState(() {
checkValue = value!;
wantKeepAlive = value;
});
}
},
);
}
}
......@@ -212,6 +212,172 @@ void main() {
testWidgets('shouldRebuild', (WidgetTester tester) async {
expect(builderDelegate.shouldRebuild(builderDelegate), isTrue);
}, variant: TargetPlatformVariant.all());
testWidgets('builder delegate supports automatic keep alive - default true', (WidgetTester tester) async {
const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0);
final ScrollController verticalController = ScrollController();
final UniqueKey checkBoxKey = UniqueKey();
final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate(
maxXIndex: 5,
maxYIndex: 5,
builder: (BuildContext context, ChildVicinity vicinity) {
return SizedBox.square(
dimension: 200,
child: Center(child: vicinity == firstCell
? KeepAliveCheckBox(key: checkBoxKey)
: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')
),
);
}
);
await tester.pumpWidget(simpleBuilderTest(
delegate: builderDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should be kept alive now.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Bring back into view, still checked, after being kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
});
testWidgets('builder delegate will not add automatic keep alives', (WidgetTester tester) async {
const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0);
final ScrollController verticalController = ScrollController();
final UniqueKey checkBoxKey = UniqueKey();
final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate(
maxXIndex: 5,
maxYIndex: 5,
addAutomaticKeepAlives: false, // No keeping alive this time
builder: (BuildContext context, ChildVicinity vicinity) {
return SizedBox.square(
dimension: 200,
child: Center(child: vicinity == firstCell
? KeepAliveCheckBox(key: checkBoxKey)
: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')
),
);
}
);
await tester.pumpWidget(simpleBuilderTest(
delegate: builderDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should not be kept alive since the
// delegate did not add automatic keep alive.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, not checked, having not been kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
});
});
group('TwoDimensionalChildListDelegate', () {
......@@ -338,6 +504,174 @@ void main() {
expect(delegate.shouldRebuild(oldDelegate), isTrue);
}, variant: TargetPlatformVariant.all());
});
testWidgets('list delegate supports automatic keep alive - default true', (WidgetTester tester) async {
final UniqueKey checkBoxKey = UniqueKey();
final Widget originCell = SizedBox.square(
dimension: 200,
child: Center(child: KeepAliveCheckBox(key: checkBoxKey)
),
);
const Widget otherCell = SizedBox.square(dimension: 200);
final ScrollController verticalController = ScrollController();
final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate(
children: <List<Widget>>[
<Widget>[originCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
],
);
await tester.pumpWidget(simpleListTest(
delegate: listDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should be kept alive now.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Bring back into view, still checked, after being kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
});
testWidgets('list delegate will not add automatic keep alives', (WidgetTester tester) async {
final UniqueKey checkBoxKey = UniqueKey();
final Widget originCell = SizedBox.square(
dimension: 200,
child: Center(child: KeepAliveCheckBox(key: checkBoxKey)
),
);
const Widget otherCell = SizedBox.square(dimension: 200);
final ScrollController verticalController = ScrollController();
final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate(
addAutomaticKeepAlives: false,
children: <List<Widget>>[
<Widget>[originCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
],
);
await tester.pumpWidget(simpleListTest(
delegate: listDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should not be kept alive since the
// delegate did not add automatic keep alive.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, not checked, having not been kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
});
});
group('TwoDimensionalScrollable', () {
......@@ -1025,7 +1359,7 @@ void main() {
expect(
parentData.toString(),
'vicinity=(xIndex: 10, yIndex: 10); layoutOffset=Offset(20.0, 20.0); '
'paintOffset=Offset(20.0, 20.0); not visible ',
'paintOffset=Offset(20.0, 20.0); not visible; ',
);
});
......
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