Skip to content

Commit af27093

Browse files
Fix memory leak in TabPageSelector (flutter#147403)
1 parent b597dd2 commit af27093

File tree

2 files changed

+81
-27
lines changed

2 files changed

+81
-27
lines changed

packages/flutter/lib/src/material/tabs.dart

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,7 +2219,7 @@ class TabPageSelectorIndicator extends StatelessWidget {
22192219
///
22202220
/// If a [TabController] is not provided, then there must be a
22212221
/// [DefaultTabController] ancestor.
2222-
class TabPageSelector extends StatelessWidget {
2222+
class TabPageSelector extends StatefulWidget {
22232223
/// Creates a compact widget that indicates which tab has been selected.
22242224
const TabPageSelector({
22252225
super.key,
@@ -2256,6 +2256,73 @@ class TabPageSelector extends StatelessWidget {
22562256
/// Defaults to [BorderStyle.solid] if value is not specified.
22572257
final BorderStyle? borderStyle;
22582258

2259+
@override
2260+
State<TabPageSelector> createState() => _TabPageSelectorState();
2261+
}
2262+
2263+
class _TabPageSelectorState extends State<TabPageSelector> {
2264+
TabController? _previousTabController;
2265+
TabController get _tabController {
2266+
final TabController? tabController = widget.controller ?? DefaultTabController.maybeOf(context);
2267+
assert(() {
2268+
if (tabController == null) {
2269+
throw FlutterError(
2270+
'No TabController for $runtimeType.\n'
2271+
'When creating a $runtimeType, you must either provide an explicit TabController '
2272+
'using the "controller" property, or you must ensure that there is a '
2273+
'DefaultTabController above the $runtimeType.\n'
2274+
'In this case, there was neither an explicit controller nor a default controller.',
2275+
);
2276+
}
2277+
return true;
2278+
}());
2279+
return tabController!;
2280+
}
2281+
2282+
CurvedAnimation? _animation;
2283+
2284+
@override
2285+
void initState() {
2286+
super.initState();
2287+
_setAnimation();
2288+
}
2289+
2290+
@override
2291+
void didUpdateWidget (TabPageSelector oldWidget) {
2292+
super.didUpdateWidget(oldWidget);
2293+
if (_previousTabController?.animation != _tabController.animation) {
2294+
_setAnimation();
2295+
}
2296+
if (_previousTabController != _tabController) {
2297+
_previousTabController = _tabController;
2298+
}
2299+
}
2300+
2301+
@override
2302+
void didChangeDependencies() {
2303+
super.didChangeDependencies();
2304+
if (_previousTabController?.animation != _tabController.animation) {
2305+
_setAnimation();
2306+
}
2307+
if (_previousTabController != _tabController) {
2308+
_previousTabController = _tabController;
2309+
}
2310+
}
2311+
2312+
void _setAnimation() {
2313+
_animation?.dispose();
2314+
_animation = CurvedAnimation(
2315+
parent: _tabController.animation!,
2316+
curve: Curves.fastOutSlowIn,
2317+
);
2318+
}
2319+
2320+
@override
2321+
void dispose() {
2322+
_animation?.dispose();
2323+
super.dispose();
2324+
}
2325+
22592326
Widget _buildTabIndicator(
22602327
int tabIndex,
22612328
TabController tabController,
@@ -2290,44 +2357,27 @@ class TabPageSelector extends StatelessWidget {
22902357
return TabPageSelectorIndicator(
22912358
backgroundColor: background,
22922359
borderColor: selectedColorTween.end!,
2293-
size: indicatorSize,
2294-
borderStyle: borderStyle ?? BorderStyle.solid,
2360+
size: widget.indicatorSize,
2361+
borderStyle: widget.borderStyle ?? BorderStyle.solid,
22952362
);
22962363
}
22972364

22982365
@override
22992366
Widget build(BuildContext context) {
2300-
final Color fixColor = color ?? Colors.transparent;
2301-
final Color fixSelectedColor = selectedColor ?? Theme.of(context).colorScheme.secondary;
2367+
final Color fixColor = widget.color ?? Colors.transparent;
2368+
final Color fixSelectedColor = widget.selectedColor ?? Theme.of(context).colorScheme.secondary;
23022369
final ColorTween selectedColorTween = ColorTween(begin: fixColor, end: fixSelectedColor);
23032370
final ColorTween previousColorTween = ColorTween(begin: fixSelectedColor, end: fixColor);
2304-
final TabController? tabController = controller ?? DefaultTabController.maybeOf(context);
23052371
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
2306-
assert(() {
2307-
if (tabController == null) {
2308-
throw FlutterError(
2309-
'No TabController for $runtimeType.\n'
2310-
'When creating a $runtimeType, you must either provide an explicit TabController '
2311-
'using the "controller" property, or you must ensure that there is a '
2312-
'DefaultTabController above the $runtimeType.\n'
2313-
'In this case, there was neither an explicit controller nor a default controller.',
2314-
);
2315-
}
2316-
return true;
2317-
}());
2318-
final Animation<double> animation = CurvedAnimation(
2319-
parent: tabController!.animation!,
2320-
curve: Curves.fastOutSlowIn,
2321-
);
23222372
return AnimatedBuilder(
2323-
animation: animation,
2373+
animation: _animation!,
23242374
builder: (BuildContext context, Widget? child) {
23252375
return Semantics(
2326-
label: localizations.tabLabel(tabIndex: tabController.index + 1, tabCount: tabController.length),
2376+
label: localizations.tabLabel(tabIndex: _tabController.index + 1, tabCount: _tabController.length),
23272377
child: Row(
23282378
mainAxisSize: MainAxisSize.min,
2329-
children: List<Widget>.generate(tabController.length, (int tabIndex) {
2330-
return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween);
2379+
children: List<Widget>.generate(_tabController.length, (int tabIndex) {
2380+
return _buildTabIndicator(tabIndex, _tabController, selectedColorTween, previousColorTween);
23312381
}).toList(),
23322382
),
23332383
);

packages/flutter/test/material/page_selector_test.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'package:flutter/material.dart';
66
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
78

89
const Color kSelectedColor = Color(0xFF00FF00);
910
const Color kUnselectedColor = Colors.transparent;
@@ -86,7 +87,10 @@ void main() {
8687
expect(indicatorColors(tester), const <Color>[kUnselectedColor, kUnselectedColor, kSelectedColor]);
8788
});
8889

89-
testWidgets('PageSelector responds correctly to TabController.animateTo()', (WidgetTester tester) async {
90+
testWidgets('PageSelector responds correctly to TabController.animateTo()',
91+
// TODO(polina-c): remove when fixed https://github.com/flutter/flutter/issues/145600 [leak-tracking-opt-in]
92+
experimentalLeakTesting: LeakTesting.settings.withTracked(classes: const <String>['CurvedAnimation']),
93+
(WidgetTester tester) async {
9094
final TabController tabController = TabController(
9195
vsync: const TestVSync(),
9296
length: 3,

0 commit comments

Comments
 (0)