Skip to content

Commit 761747b

Browse files
authored
[CP] Fix TwoDimensionalViewport's keep alive child not always removed (when no longer should be kept alive) (flutter#149639)
Cherry pick request of flutter#148298 to stable. Fixes problems in 2D viewport when using keep alive widgets (affects users using Material Ink components).
1 parent e3f15a5 commit 761747b

File tree

3 files changed

+100
-0
lines changed

3 files changed

+100
-0
lines changed

packages/flutter/lib/src/widgets/two_dimensional_viewport.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,10 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
16471647
_children.remove(slot);
16481648
}
16491649
assert(_debugTrackOrphans(noLongerOrphan: child));
1650+
if (_keepAliveBucket[childParentData.vicinity] == child) {
1651+
_keepAliveBucket.remove(childParentData.vicinity);
1652+
}
1653+
assert(_keepAliveBucket[childParentData.vicinity] != child);
16501654
dropChild(child);
16511655
return;
16521656
}

packages/flutter/test/widgets/two_dimensional_utils.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,39 @@ class TestParentDataWidget extends ParentDataWidget<TestExtendedParentData> {
510510
@override
511511
Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport;
512512
}
513+
514+
class KeepAliveOnlyWhenHovered extends StatefulWidget {
515+
const KeepAliveOnlyWhenHovered({ required this.child, super.key });
516+
517+
final Widget child;
518+
519+
@override
520+
KeepAliveOnlyWhenHoveredState createState() => KeepAliveOnlyWhenHoveredState();
521+
}
522+
523+
class KeepAliveOnlyWhenHoveredState extends State<KeepAliveOnlyWhenHovered> with AutomaticKeepAliveClientMixin {
524+
bool _hovered = false;
525+
526+
@override
527+
bool get wantKeepAlive => _hovered;
528+
529+
@override
530+
Widget build(BuildContext context) {
531+
super.build(context);
532+
return MouseRegion(
533+
onEnter: (_) {
534+
setState(() {
535+
_hovered = true;
536+
updateKeepAlive();
537+
});
538+
},
539+
onExit: (_) {
540+
setState(() {
541+
_hovered = false;
542+
updateKeepAlive();
543+
});
544+
},
545+
child: widget.child,
546+
);
547+
}
548+
}

packages/flutter/test/widgets/two_dimensional_viewport_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,66 @@ void main() {
731731
);
732732
});
733733

734+
testWidgets('Ensure KeepAlive widget is not held onto when it no longer should be kept alive offscreen', (WidgetTester tester) async {
735+
// Regression test for https://github.com/flutter/flutter/issues/138977
736+
final UniqueKey checkBoxKey = UniqueKey();
737+
final Widget originCell = KeepAliveOnlyWhenHovered(
738+
key: checkBoxKey,
739+
child: const SizedBox.square(dimension: 200),
740+
);
741+
const Widget otherCell = SizedBox.square(dimension: 200, child: Placeholder());
742+
final ScrollController verticalController = ScrollController();
743+
addTearDown(verticalController.dispose);
744+
final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate(
745+
children: <List<Widget>>[
746+
<Widget>[originCell, otherCell, otherCell, otherCell, otherCell],
747+
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
748+
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
749+
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
750+
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
751+
],
752+
);
753+
addTearDown(listDelegate.dispose);
754+
755+
await tester.pumpWidget(simpleListTest(
756+
delegate: listDelegate,
757+
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
758+
));
759+
await tester.pumpAndSettle();
760+
expect(find.byKey(checkBoxKey), findsOneWidget);
761+
762+
// Scroll away, should not be kept alive (disposed).
763+
verticalController.jumpTo(verticalController.position.maxScrollExtent);
764+
await tester.pump();
765+
expect(find.byKey(checkBoxKey), findsNothing);
766+
767+
// Bring back into view
768+
verticalController.jumpTo(0.0);
769+
await tester.pump();
770+
expect(find.byKey(checkBoxKey), findsOneWidget);
771+
772+
// Hover over widget to make it keep alive.
773+
final TestGesture gesture = await tester.createGesture(
774+
kind: PointerDeviceKind.mouse,
775+
);
776+
await gesture.addPointer(location: Offset.zero);
777+
addTearDown(gesture.removePointer);
778+
await tester.pump();
779+
await gesture.moveTo(tester.getCenter(find.byKey(checkBoxKey)));
780+
await tester.pump();
781+
782+
// Scroll away, should be kept alive still.
783+
verticalController.jumpTo(verticalController.position.maxScrollExtent);
784+
await tester.pump();
785+
expect(find.byKey(checkBoxKey), findsOneWidget);
786+
787+
// Move the pointer outside the widget bounds to trigger exit event
788+
// and remove it from keep alive bucket.
789+
await gesture.moveTo(const Offset(300, 300));
790+
await tester.pump();
791+
expect(find.byKey(checkBoxKey), findsNothing);
792+
});
793+
734794
testWidgets('list delegate will not add automatic keep alives', (WidgetTester tester) async {
735795
final UniqueKey checkBoxKey = UniqueKey();
736796
final Widget originCell = SizedBox.square(

0 commit comments

Comments
 (0)