Skip to content

Commit 03e6cfa

Browse files
authored
Add test for tab_controller.1.dart API example. (#148189)
This PR contributes to flutter/flutter#130459 ### Description - Updates `examples/api/lib/material/tab_controller/tab_controller.1.dart` to properly remove the listener from the `TabController` - Adds tests for `examples/api/lib/material/tab_controller/tab_controller.1.dart`
1 parent 6067d8f commit 03e6cfa

File tree

3 files changed

+184
-25
lines changed

3 files changed

+184
-25
lines changed

dev/bots/check_code_samples.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,6 @@ final Set<String> _knownMissingTests = <String>{
346346
'examples/api/test/material/search_anchor/search_anchor.1_test.dart',
347347
'examples/api/test/material/search_anchor/search_anchor.2_test.dart',
348348
'examples/api/test/material/about/about_list_tile.0_test.dart',
349-
'examples/api/test/material/tab_controller/tab_controller.1_test.dart',
350349
'examples/api/test/material/selection_area/selection_area.0_test.dart',
351350
'examples/api/test/material/scaffold/scaffold.end_drawer.0_test.dart',
352351
'examples/api/test/material/scaffold/scaffold.drawer.0_test.dart',

examples/api/lib/material/tab_controller/tab_controller.1.dart

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,39 @@ void main() => runApp(const TabControllerExampleApp());
1111
class TabControllerExampleApp extends StatelessWidget {
1212
const TabControllerExampleApp({super.key});
1313

14+
static const List<Tab> tabs = <Tab>[
15+
Tab(text: 'Zeroth'),
16+
Tab(text: 'First'),
17+
Tab(text: 'Second'),
18+
];
19+
1420
@override
1521
Widget build(BuildContext context) {
1622
return const MaterialApp(
17-
home: TabControllerExample(),
23+
home: TabControllerExample(tabs: tabs),
1824
);
1925
}
2026
}
2127

22-
const List<Tab> tabs = <Tab>[
23-
Tab(text: 'Zeroth'),
24-
Tab(text: 'First'),
25-
Tab(text: 'Second'),
26-
];
27-
2828
class TabControllerExample extends StatelessWidget {
29-
const TabControllerExample({super.key});
29+
const TabControllerExample({
30+
required this.tabs,
31+
super.key,
32+
});
33+
34+
final List<Tab> tabs;
3035

3136
@override
3237
Widget build(BuildContext context) {
3338
return DefaultTabController(
3439
length: tabs.length,
35-
// The Builder widget is used to have a different BuildContext to access
36-
// closest DefaultTabController.
37-
child: Builder(builder: (BuildContext context) {
38-
final TabController tabController = DefaultTabController.of(context);
39-
tabController.addListener(() {
40-
if (!tabController.indexIsChanging) {
41-
// Your code goes here.
42-
// To get index of current tab use tabController.index
43-
}
44-
});
45-
return Scaffold(
40+
child: DefaultTabControllerListener(
41+
onTabChanged: (int index) {
42+
debugPrint('tab changed: $index');
43+
},
44+
child: Scaffold(
4645
appBar: AppBar(
47-
bottom: const TabBar(
48-
tabs: tabs,
49-
),
46+
bottom: TabBar(tabs: tabs),
5047
),
5148
body: TabBarView(
5249
children: tabs.map((Tab tab) {
@@ -58,8 +55,75 @@ class TabControllerExample extends StatelessWidget {
5855
);
5956
}).toList(),
6057
),
61-
);
62-
}),
58+
),
59+
),
6360
);
6461
}
6562
}
63+
64+
class DefaultTabControllerListener extends StatefulWidget {
65+
const DefaultTabControllerListener({
66+
required this.onTabChanged,
67+
required this.child,
68+
super.key,
69+
});
70+
71+
final ValueChanged<int> onTabChanged;
72+
73+
final Widget child;
74+
75+
@override
76+
State<DefaultTabControllerListener> createState() =>
77+
_DefaultTabControllerListenerState();
78+
}
79+
80+
class _DefaultTabControllerListenerState
81+
extends State<DefaultTabControllerListener> {
82+
TabController? _controller;
83+
84+
@override
85+
void didChangeDependencies() {
86+
super.didChangeDependencies();
87+
88+
final TabController? defaultTabController =
89+
DefaultTabController.maybeOf(context);
90+
91+
assert(() {
92+
if (defaultTabController == null) {
93+
throw FlutterError(
94+
'No DefaultTabController for ${widget.runtimeType}.\n'
95+
'When creating a ${widget.runtimeType}, you must ensure that there '
96+
'is a DefaultTabController above the ${widget.runtimeType}.',
97+
);
98+
}
99+
return true;
100+
}());
101+
102+
if (defaultTabController != _controller) {
103+
_controller?.removeListener(_listener);
104+
_controller = defaultTabController;
105+
_controller?.addListener(_listener);
106+
}
107+
}
108+
109+
void _listener() {
110+
final TabController? controller = _controller;
111+
112+
if (controller == null || controller.indexIsChanging) {
113+
return;
114+
}
115+
116+
widget.onTabChanged(controller.index);
117+
}
118+
119+
@override
120+
void dispose() {
121+
_controller?.removeListener(_listener);
122+
super.dispose();
123+
}
124+
125+
@override
126+
Widget build(BuildContext context) {
127+
return widget.child;
128+
}
129+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_api_samples/material/tab_controller/tab_controller.1.dart'
8+
as example;
9+
import 'package:flutter_test/flutter_test.dart';
10+
11+
void main() {
12+
testWidgets('Verify first tab is selected by default', (WidgetTester tester) async {
13+
await tester.pumpWidget(
14+
const example.TabControllerExampleApp(),
15+
);
16+
17+
final Tab firstTab = example.TabControllerExampleApp.tabs.first;
18+
19+
expect(
20+
find.descendant(
21+
of: find.byType(TabBarView),
22+
matching: find.text('${firstTab.text} Tab'),
23+
),
24+
findsOneWidget,
25+
);
26+
});
27+
28+
testWidgets('Verify tabs can be changed', (WidgetTester tester) async {
29+
final List<String?> log = <String?>[];
30+
31+
final DebugPrintCallback originalDebugPrint = debugPrint;
32+
debugPrint = (String? message, {int? wrapWidth}) {
33+
log.add(message);
34+
};
35+
36+
await tester.pumpWidget(
37+
const example.TabControllerExampleApp(),
38+
);
39+
40+
const List<Tab> tabs = example.TabControllerExampleApp.tabs;
41+
final List<Tab> tabsTraversalOrder = <Tab>[];
42+
43+
// The traverse order is from the second tab from the start to the last,
44+
// and then from the second tab from the end to the first.
45+
tabsTraversalOrder.addAll(tabs.skip(1));
46+
tabsTraversalOrder.addAll(tabs.reversed.skip(1));
47+
48+
for (final Tab tab in tabsTraversalOrder) {
49+
// Tap on the TabBar's tab to select it.
50+
await tester.tap(find.descendant(
51+
of: find.byType(TabBar),
52+
matching: find.text(tab.text!),
53+
));
54+
await tester.pumpAndSettle();
55+
56+
expect(
57+
find.descendant(
58+
of: find.byType(TabBarView),
59+
matching: find.text('${tab.text} Tab'),
60+
),
61+
findsOneWidget,
62+
);
63+
64+
expect(log.length, equals(1));
65+
expect(log.last, equals('tab changed: ${tabs.indexOf(tab)}'));
66+
67+
log.clear();
68+
}
69+
70+
debugPrint = originalDebugPrint;
71+
});
72+
73+
testWidgets('DefaultTabControllerListener throws when no DefaultTabController above', (WidgetTester tester) async {
74+
await tester.pumpWidget(
75+
example.DefaultTabControllerListener(
76+
onTabChanged: (_) {},
77+
child: const SizedBox.shrink(),
78+
),
79+
);
80+
81+
final dynamic exception = tester.takeException();
82+
expect(exception, isFlutterError);
83+
84+
final FlutterError error = exception as FlutterError;
85+
expect(
86+
error.toStringDeep(),
87+
equalsIgnoringHashCodes(
88+
'FlutterError\n'
89+
' No DefaultTabController for DefaultTabControllerListener.\n'
90+
' When creating a DefaultTabControllerListener, you must ensure\n'
91+
' that there is a DefaultTabController above the\n'
92+
' DefaultTabControllerListener.\n',
93+
),
94+
);
95+
});
96+
}

0 commit comments

Comments
 (0)