// 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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ const WidgetTest0(text: 'child 0', key: ValueKey(0), keepAlive: true), const WidgetTest1(text: 'child 1', key: ValueKey(1), keepAlive: true), const WidgetTest2(text: 'child 2', key: ValueKey(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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ 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 childList= [ 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 createSwitchedChildList(List childList, int i, int j) { final Widget w = childList[i]; childList[i] = childList[j]; childList[j] = w; return List.from(childList); } class SwitchingChildBuilderTest extends StatefulWidget { const SwitchingChildBuilderTest({ required this.children, super.key, }); final List children; @override State createState() => _SwitchingChildBuilderTest(); } class _SwitchingChildBuilderTest extends State { late List children; late Map _mapKeyToIndex; @override void initState() { super.initState(); children = widget.children; _mapKeyToIndex = {}; 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 = {}; 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: [ SliverFillViewport( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return children[index]; }, childCount: children.length, findChildIndexCallback: (Key key) => _mapKeyToIndex[key] ?? -1, ), ), ], ), ), ), ); } } class SwitchingChildListTest extends StatefulWidget { const SwitchingChildListTest({ required this.children, this.viewportFraction = 1.0, super.key, }); final List children; final double viewportFraction; @override State createState() => _SwitchingChildListTest(); } class _SwitchingChildListTest extends State { @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( cacheExtent: 0, slivers: [ SliverFillViewport( viewportFraction: widget.viewportFraction, delegate: SliverChildListDelegate(widget.children), ), ], ), ), ), ); } } class SwitchingSliverListTest extends StatefulWidget { const SwitchingSliverListTest({ required this.children, this.viewportFraction = 1.0, super.key, }); final List children; final double viewportFraction; @override State createState() => _SwitchingSliverListTest(); } class _SwitchingSliverListTest extends State { @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Center( child: SizedBox( height: 100, child: CustomScrollView( cacheExtent: 0, slivers: [ SliverList( delegate: SliverChildListDelegate(widget.children), ), ], ), ), ), ); } } class WidgetTest0 extends StatefulWidget { const WidgetTest0({ required this.text, this.keepAlive = false, super.key, }); final String text; final bool keepAlive; @override State createState() => _WidgetTest0State(); } class _WidgetTest0State extends State 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 createState() => _WidgetTest1State(); } class _WidgetTest1State extends State 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 createState() => _WidgetTest2State(); } class _WidgetTest2State extends State 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; }