Unverified Commit 91478d96 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Do not instantiate intermediate tabs during transition (#68124)

parent 52c715fe
......@@ -861,6 +861,9 @@ class SliverList extends SliverMultiBoxAdaptorWidget {
required SliverChildDelegate delegate,
}) : super(key: key, delegate: delegate);
@override
SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true);
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
......@@ -1079,7 +1082,21 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget {
/// the children of subclasses of [RenderSliverMultiBoxAdaptor].
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
/// Creates an element that lazily builds children for the given widget.
SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget) : super(widget);
///
/// If `replaceMovedChildren` is set to true, a new child is proactively
/// inflate for the index that was previously occupied by a child that moved
/// to a new index. The layout offset of the moved child is copied over to the
/// new child. RenderObjects, that depend on the layout offset of existing
/// children during [RenderObject.performLayout] should set this to true
/// (example: [RenderSliverList]). For RenderObjects that figure out the
/// layout offset of their children without looking at the layout offset of
/// existing children this should be set to false (example:
/// [RenderSliverFixedExtentList]) to avoid inflating unnecessary children.
SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget, {bool replaceMovedChildren = false})
: _replaceMovedChildren = replaceMovedChildren,
super(widget);
final bool _replaceMovedChildren;
@override
SliverMultiBoxAdaptorWidget get widget => super.widget as SliverMultiBoxAdaptorWidget;
......@@ -1146,8 +1163,10 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
childParentData.layoutOffset = null;
newChildren[newIndex] = _childElements[index];
// We need to make sure the original index gets processed.
newChildren.putIfAbsent(index, () => null);
if (_replaceMovedChildren) {
// We need to make sure the original index gets processed.
newChildren.putIfAbsent(index, () => null);
}
// We do not want the remapped child to get deactivated during processElement.
_childElements.remove(index);
} else {
......
......@@ -2771,6 +2771,52 @@ void main() {
await tester.pump();
expect(tabController.animation!.value, pageController.page);
});
testWidgets('Does not instantiate intermediate tabs during animation', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/14316.
final List<String> log = <String>[];
await tester.pumpWidget(MaterialApp(
home: DefaultTabController(
length: 5,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: <Widget>[
Tab(text: 'car'),
Tab(text: 'transit'),
Tab(text: 'bike'),
Tab(text: 'boat'),
Tab(text: 'bus'),
],
),
title: const Text('Tabs Test'),
),
body: TabBarView(
children: <Widget>[
TabBody(index: 0, log: log),
TabBody(index: 1, log: log),
TabBody(index: 2, log: log),
TabBody(index: 3, log: log),
TabBody(index: 4, log: log),
],
),
),
),
));
expect(find.text('0'), findsOneWidget);
expect(find.text('3'), findsNothing);
expect(log, <String>['init: 0']);
await tester.tap(find.text('boat'));
await tester.pumpAndSettle();
expect(find.text('0'), findsNothing);
expect(find.text('3'), findsOneWidget);
// No other tab got instantiated during the animation.
expect(log, <String>['init: 0', 'init: 3', 'dispose: 0']);
});
}
class KeepAliveInk extends StatefulWidget {
......@@ -2826,3 +2872,41 @@ class TabBarDemo extends StatelessWidget {
}
class MockScrollMetrics extends Fake implements ScrollMetrics {}
class TabBody extends StatefulWidget {
const TabBody({ Key? key, required this.index, required this.log }) : super(key: key);
final int index;
final List<String> log;
@override
State<TabBody> createState() => TabBodyState();
}
class TabBodyState extends State<TabBody> {
@override
void initState() {
widget.log.add('init: ${widget.index}');
super.initState();
}
@override
void didUpdateWidget(TabBody oldWidget) {
super.didUpdateWidget(oldWidget);
// To keep the logging straight, widgets must not change their index.
assert(oldWidget.index == widget.index);
}
@override
void dispose() {
widget.log.add('dispose: ${widget.index}');
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Text('${widget.index}'),
);
}
}
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