blob: 5065232cbe1807778ff069b3983667583d4f0e3d [file] [log] [blame]
// 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;
}