// Copyright 2014 The Flutter 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/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Sliver with keep alive without key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0', keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), const WidgetTest2(text: 'child 2', keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, true); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver without keep alive without key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0'), const WidgetTest1(text: 'child 1'), const WidgetTest2(text: 'child 2'), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, true); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver without keep alive with key - should dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey()), WidgetTest1(text: 'child 1', key: GlobalKey()), WidgetTest2(text: 'child 2', key: GlobalKey()), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, true); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver with keep alive with key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true), WidgetTest2(text: 'child 2', key: GlobalKey(), keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(state0.hasBeenDisposed, false); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver with keep alive with Unique key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey(), keepAlive: true), WidgetTest2(text: 'child 2', key: UniqueKey(), keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(state0.hasBeenDisposed, false); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver with keep alive with Value key - should not dispose after reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ const WidgetTest0(text: 'child 0', key: ValueKey<int>(0), keepAlive: true), const WidgetTest1(text: 'child 1', key: ValueKey<int>(1), keepAlive: true), const WidgetTest2(text: 'child 2', key: ValueKey<int>(2), keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(state0.hasBeenDisposed, false); expect(state2.hasBeenDisposed, false); }); testWidgets('Sliver complex case 1', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey(), keepAlive: true), const WidgetTest2(text: 'child 2', keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildListTest(children: childList)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, false); // Child 2 does not have a key. expect(state2.hasBeenDisposed, true); }); testWidgets('Sliver complex case 2', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: GlobalKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey()), const WidgetTest2(text: 'child 2', keepAlive: true), ]; await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildListTest(children: childList)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget(SwitchingChildListTest(children: childList)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildListTest(children: childList)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, true); expect(state2.hasBeenDisposed, true); }); testWidgets('Sliver with SliverChildBuilderDelegate', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: GlobalKey()), const WidgetTest2(text: 'child 2', keepAlive: true), ]; await tester.pumpWidget(SwitchingChildBuilderTest(children: childList)); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget(SwitchingChildBuilderTest(children: childList)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 2'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildBuilderTest(children: childList)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(find.text('child 0', skipOffstage: false), findsOneWidget); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget(SwitchingChildBuilderTest(children: childList)); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 0', skipOffstage: false), findsOneWidget); expect(find.text('child 2', skipOffstage: false), findsNothing); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget(SwitchingChildBuilderTest(children: childList)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1', skipOffstage: false), findsNothing); expect(find.text('child 2', skipOffstage: false), findsNothing); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, true); expect(state2.hasBeenDisposed, true); }); testWidgets('SliverFillViewport should not dispose widget with key during in screen reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), WidgetTest1(text: 'child 1', key: UniqueKey()), const WidgetTest2(text: 'child 2', keepAlive: true), ]; await tester.pumpWidget( SwitchingChildListTest(viewportFraction: 0.1, children: childList), ); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2'), findsOneWidget); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget( SwitchingChildListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget( SwitchingChildListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget( SwitchingChildListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget( SwitchingChildListTest(viewportFraction: 0.1, children: childList), ); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, false); expect(state2.hasBeenDisposed, true); }); testWidgets('SliverList should not dispose widget with key during in screen reordering', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), WidgetTest2(text: 'child 2', key: UniqueKey()), ]; await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2'), findsOneWidget); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 1, 2); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 0, 2); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); childList = createSwitchedChildList(childList, 0, 1); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, true); expect(state2.hasBeenDisposed, false); }); testWidgets('SliverList remove child from child list', (WidgetTester tester) async { List<Widget> childList= <Widget>[ WidgetTest0(text: 'child 0', key: UniqueKey(), keepAlive: true), const WidgetTest1(text: 'child 1', keepAlive: true), WidgetTest2(text: 'child 2', key: UniqueKey()), ]; await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); final _WidgetTest0State state0 = tester.state(find.byType(WidgetTest0)); final _WidgetTest1State state1 = tester.state(find.byType(WidgetTest1)); final _WidgetTest2State state2 = tester.state(find.byType(WidgetTest2)); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2'), findsOneWidget); childList = createSwitchedChildList(childList, 0, 1); childList.removeAt(2); await tester.pumpWidget( SwitchingSliverListTest(viewportFraction: 0.1, children: childList), ); expect(find.text('child 0'), findsOneWidget); expect(find.text('child 1'), findsOneWidget); expect(find.text('child 2'), findsNothing); expect(state0.hasBeenDisposed, false); expect(state1.hasBeenDisposed, true); expect(state2.hasBeenDisposed, true); }); } List<Widget> createSwitchedChildList(List<Widget> childList, int i, int j) { final Widget w = childList[i]; childList[i] = childList[j]; childList[j] = w; return List<Widget>.from(childList); } class SwitchingChildBuilderTest extends StatefulWidget { const SwitchingChildBuilderTest({ required this.children, super.key, }); final List<Widget> children; @override State<SwitchingChildBuilderTest> createState() => _SwitchingChildBuilderTest(); } class _SwitchingChildBuilderTest extends State<SwitchingChildBuilderTest> { late List<Widget> children; late Map<Key, int> _mapKeyToIndex; @override void initState() { super.initState(); children = widget.children; _mapKeyToIndex = <Key, int>{}; for (int index = 0; index < children.length; index += 1) { final Key? key = children[index].key; if (key != null) { _mapKeyToIndex[key] = index; } } } @override void didUpdateWidget(SwitchingChildBuilderTest oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.children != widget.children) { children = widget.children; _mapKeyToIndex = <Key, int>{}; for (int index = 0; index < children.length; index += 1) { final Key? key = children[index].key; if (key != null) { _mapKeyToIndex[key] = index; } } } } @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( cacheExtent: 0, slivers: <Widget>[ SliverFillViewport( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return children[index]; }, childCount: children.length, findChildIndexCallback: (Key key) => _mapKeyToIndex[key] ?? -1, ), ), ], ), ), ), ); } } class SwitchingChildListTest extends StatelessWidget { const SwitchingChildListTest({ required this.children, this.viewportFraction = 1.0, super.key, }); final List<Widget> children; final double viewportFraction; @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( cacheExtent: 0, slivers: <Widget>[ SliverFillViewport( viewportFraction: viewportFraction, delegate: SliverChildListDelegate(children), ), ], ), ), ), ); } } class SwitchingSliverListTest extends StatelessWidget { const SwitchingSliverListTest({ required this.children, this.viewportFraction = 1.0, super.key, }); final List<Widget> children; final double viewportFraction; @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( cacheExtent: 0, slivers: <Widget>[ SliverList( delegate: SliverChildListDelegate(children), ), ], ), ), ), ); } } class WidgetTest0 extends StatefulWidget { const WidgetTest0({ required this.text, this.keepAlive = false, super.key, }); final String text; final bool keepAlive; @override State<WidgetTest0> createState() => _WidgetTest0State(); } class _WidgetTest0State extends State<WidgetTest0> with AutomaticKeepAliveClientMixin { bool hasBeenDisposed = false; @override Widget build(BuildContext context) { super.build(context); return Text(widget.text); } @override void dispose() { hasBeenDisposed = true; super.dispose(); } @override bool get wantKeepAlive => widget.keepAlive; } class WidgetTest1 extends StatefulWidget { const WidgetTest1({ required this.text, this.keepAlive = false, super.key, }); final String text; final bool keepAlive; @override State<WidgetTest1> createState() => _WidgetTest1State(); } class _WidgetTest1State extends State<WidgetTest1> with AutomaticKeepAliveClientMixin { bool hasBeenDisposed = false; @override Widget build(BuildContext context) { super.build(context); return Text(widget.text); } @override void dispose() { hasBeenDisposed = true; super.dispose(); } @override bool get wantKeepAlive => widget.keepAlive; } class WidgetTest2 extends StatefulWidget { const WidgetTest2({ required this.text, this.keepAlive = false, super.key, }); final String text; final bool keepAlive; @override State<WidgetTest2> createState() => _WidgetTest2State(); } class _WidgetTest2State extends State<WidgetTest2> with AutomaticKeepAliveClientMixin { bool hasBeenDisposed = false; @override Widget build(BuildContext context) { super.build(context); return Text(widget.text); } @override void dispose() { hasBeenDisposed = true; super.dispose(); } @override bool get wantKeepAlive => widget.keepAlive; }