Add CupertinoTabController (#31227)
Add CupertinoTabController that allows a CupertinoTabScaffold's current page to be controlled from an ancestor widget.
This commit is contained in:
parent
a0ed52caa6
commit
8fa470f38c
@ -6,6 +6,100 @@ import 'package:flutter/widgets.dart';
|
||||
import 'bottom_tab_bar.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Coordinates tab selection between a [CupertinoTabBar] and a [CupertinoTabScaffold].
|
||||
///
|
||||
/// The [index] property is the index of the selected tab. Changing its value
|
||||
/// updates the actively displayed tab of the [CupertinoTabScaffold] the
|
||||
/// [CupertinoTabController] controls, as well as the currently selected tab item of
|
||||
/// its [CupertinoTabBar].
|
||||
///
|
||||
/// {@tool sample}
|
||||
///
|
||||
/// [CupertinoTabController] can be used to switch tabs:
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyCupertinoTabScaffoldPage extends StatefulWidget {
|
||||
/// @override
|
||||
/// _CupertinoTabScaffoldPageState createState() => _CupertinoTabScaffoldPageState();
|
||||
/// }
|
||||
///
|
||||
/// class _CupertinoTabScaffoldPageState extends State<MyCupertinoTabScaffoldPage> {
|
||||
/// final CupertinoTabController _controller = CupertinoTabController();
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return CupertinoTabScaffold(
|
||||
/// tabBar: CupertinoTabBar(
|
||||
/// items: <BottomNavigationBarItem> [
|
||||
/// // ...
|
||||
/// ],
|
||||
/// ),
|
||||
/// controller: _controller,
|
||||
/// tabBuilder: (BuildContext context, int index) {
|
||||
/// return Center(
|
||||
/// child: CupertinoButton(
|
||||
/// child: const Text('Go to first tab'),
|
||||
/// onPressed: () => _controller.index = 0,
|
||||
/// )
|
||||
/// );
|
||||
/// }
|
||||
/// );
|
||||
/// }
|
||||
///
|
||||
/// @override
|
||||
/// void dispose() {
|
||||
/// _controller.dispose();
|
||||
/// super.dispose();
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoTabScaffold], a tabbed application root layout that can be
|
||||
/// controlled by a [CupertinoTabController].
|
||||
class CupertinoTabController extends ChangeNotifier {
|
||||
/// Creates a [CupertinoTabController] to control the tab index of [CupertinoTabScaffold]
|
||||
/// and [CupertinoTabBar].
|
||||
///
|
||||
/// The [initialIndex] must not be null and defaults to 0. The value must be
|
||||
/// greater than or equal to 0, and less than the total number of tabs.
|
||||
CupertinoTabController({ int initialIndex = 0 })
|
||||
: _index = initialIndex,
|
||||
assert(initialIndex != null),
|
||||
assert(initialIndex >= 0);
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
/// The index of the currently selected tab.
|
||||
///
|
||||
/// Changing the value of [index] updates the actively displayed tab of the
|
||||
/// [CupertinoTabScaffold] controlled by this [CupertinoTabController], as well
|
||||
/// as the currently selected tab item of its [CupertinoTabScaffold.tabBar].
|
||||
///
|
||||
/// The value must be greater than or equal to 0, and less than the total
|
||||
/// number of tabs.
|
||||
int get index => _index;
|
||||
int _index;
|
||||
set index(int value) {
|
||||
assert(value != null);
|
||||
assert(value >= 0);
|
||||
if (_index == value) {
|
||||
return;
|
||||
}
|
||||
_index = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements a tabbed iOS application's root layout and behavior structure.
|
||||
///
|
||||
/// The scaffold lays out the tab bar at the bottom and the content between or
|
||||
@ -15,6 +109,12 @@ import 'theme.dart';
|
||||
/// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks
|
||||
/// to change the active tab.
|
||||
///
|
||||
/// A [controller] can be used to provide an initially selected tab index and manage
|
||||
/// subsequent tab changes. If a controller is not specified, the scaffold will
|
||||
/// create its own [CupertinoTabController] and manage it internally. Otherwise
|
||||
/// it's up to the owner of [controller] to call `dispose` on it after finish
|
||||
/// using it.
|
||||
///
|
||||
/// Tabs' contents are built with the provided [tabBuilder] at the active
|
||||
/// tab index. The [tabBuilder] must be able to build the same number of
|
||||
/// pages as there are [tabBar.items]. Inactive tabs will be moved [Offstage]
|
||||
@ -87,6 +187,7 @@ import 'theme.dart';
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoTabBar], the bottom tab bar inserted in the scaffold.
|
||||
/// * [CupertinoTabController], the selection state of this widget
|
||||
/// * [CupertinoTabView], the typical root content of each tab that holds its own
|
||||
/// [Navigator] stack.
|
||||
/// * [CupertinoPageRoute], a route hosting modal pages with iOS style transitions.
|
||||
@ -96,27 +197,35 @@ class CupertinoTabScaffold extends StatefulWidget {
|
||||
/// Creates a layout for applications with a tab bar at the bottom.
|
||||
///
|
||||
/// The [tabBar] and [tabBuilder] arguments must not be null.
|
||||
const CupertinoTabScaffold({
|
||||
CupertinoTabScaffold({
|
||||
Key key,
|
||||
@required this.tabBar,
|
||||
@required this.tabBuilder,
|
||||
this.controller,
|
||||
this.backgroundColor,
|
||||
this.resizeToAvoidBottomInset = true,
|
||||
}) : assert(tabBar != null),
|
||||
assert(tabBuilder != null),
|
||||
assert(
|
||||
controller == null || controller.index < tabBar.items.length,
|
||||
"The CupertinoTabController's current index ${controller.index} is "
|
||||
'out of bounds for the tab bar with ${tabBar.items.length} tabs'
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// The [tabBar] is a [CupertinoTabBar] drawn at the bottom of the screen
|
||||
/// that lets the user switch between different tabs in the main content area
|
||||
/// when present.
|
||||
///
|
||||
/// Setting and changing [CupertinoTabBar.currentIndex] programmatically will
|
||||
/// change the currently selected tab item in the [tabBar] as well as change
|
||||
/// the currently focused tab from the [tabBuilder].
|
||||
|
||||
/// The [CupertinoTabBar.currentIndex] is only used to initialize a
|
||||
/// [CupertinoTabController] when no [controller] is provided. Subsequently
|
||||
/// providing a different [CupertinoTabBar.currentIndex] does not affect the
|
||||
/// scaffold or the tab bar's active tab index. To programmatically change
|
||||
/// the active tab index, use a [CupertinoTabController].
|
||||
///
|
||||
/// If [CupertinoTabBar.onTap] is provided, it will still be called.
|
||||
/// [CupertinoTabScaffold] automatically also listen to the
|
||||
/// [CupertinoTabBar]'s `onTap` to change the [CupertinoTabBar]'s `currentIndex`
|
||||
/// [CupertinoTabBar]'s `onTap` to change the [controller]'s `index`
|
||||
/// and change the actively displayed tab in [CupertinoTabScaffold]'s own
|
||||
/// main content area.
|
||||
///
|
||||
@ -126,6 +235,14 @@ class CupertinoTabScaffold extends StatefulWidget {
|
||||
/// Must not be null.
|
||||
final CupertinoTabBar tabBar;
|
||||
|
||||
/// Controls the currently selected tab index of the [tabBar], as well as the
|
||||
/// active tab index of the [tabBuilder]. Providing a different [controller]
|
||||
/// will also update the scaffold's current active index to the new controller's
|
||||
/// index value.
|
||||
///
|
||||
/// Defaults to null.
|
||||
final CupertinoTabController controller;
|
||||
|
||||
/// An [IndexedWidgetBuilder] that's called when tabs become active.
|
||||
///
|
||||
/// The widgets built by [IndexedWidgetBuilder] is typically a [CupertinoTabView]
|
||||
@ -162,29 +279,55 @@ class CupertinoTabScaffold extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
int _currentPage;
|
||||
CupertinoTabController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentPage = widget.tabBar.currentIndex;
|
||||
_updateTabController();
|
||||
}
|
||||
|
||||
void _updateTabController({ bool shouldDisposeOldController = false }) {
|
||||
final CupertinoTabController newController =
|
||||
// User provided a new controller, update `_controller` with it.
|
||||
widget.controller
|
||||
?? CupertinoTabController(initialIndex: widget.tabBar.currentIndex);
|
||||
|
||||
if (newController == _controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldDisposeOldController) {
|
||||
_controller?.dispose();
|
||||
} else if (_controller?._isDisposed == false) {
|
||||
_controller.removeListener(_onCurrentIndexChange);
|
||||
}
|
||||
|
||||
newController.addListener(_onCurrentIndexChange);
|
||||
_controller = newController;
|
||||
}
|
||||
|
||||
void _onCurrentIndexChange() {
|
||||
assert(
|
||||
_controller.index >= 0 && _controller.index < widget.tabBar.items.length,
|
||||
"The $runtimeType's current index ${_controller.index} is "
|
||||
'out of bounds for the tab bar with ${widget.tabBar.items.length} tabs'
|
||||
);
|
||||
|
||||
// The value of `_controller.index` has already been updated at this point.
|
||||
// Calling `setState` to rebuild using `_controller.index`.
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CupertinoTabScaffold oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (_currentPage >= widget.tabBar.items.length) {
|
||||
// Clip down to an acceptable range.
|
||||
_currentPage = widget.tabBar.items.length - 1;
|
||||
// Sanity check, since CupertinoTabBar.items's minimum length is 2.
|
||||
assert(
|
||||
_currentPage >= 0,
|
||||
'CupertinoTabBar is expected to keep at least 2 tabs after updating',
|
||||
);
|
||||
}
|
||||
// The user can still specify an exact desired index.
|
||||
if (widget.tabBar.currentIndex != oldWidget.tabBar.currentIndex) {
|
||||
_currentPage = widget.tabBar.currentIndex;
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController(shouldDisposeOldController: oldWidget.controller == null);
|
||||
} else if (_controller.index >= widget.tabBar.items.length) {
|
||||
// If a new [tabBar] with less than (_controller.index + 1) items is provided,
|
||||
// clamp the current index.
|
||||
_controller.index = widget.tabBar.items.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,7 +339,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
MediaQueryData newMediaQuery = MediaQuery.of(context);
|
||||
|
||||
Widget content = _TabSwitchingView(
|
||||
currentTabIndex: _currentPage,
|
||||
currentTabIndex: _controller.index,
|
||||
tabNumber: widget.tabBar.items.length,
|
||||
tabBuilder: widget.tabBuilder,
|
||||
);
|
||||
@ -248,14 +391,12 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
stacked.add(Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
// Override the tab bar's currentIndex to the current tab and hook in
|
||||
// our own listener to update the _currentPage on top of a possibly user
|
||||
// our own listener to update the [_controller.currentIndex] on top of a possibly user
|
||||
// provided callback.
|
||||
child: widget.tabBar.copyWith(
|
||||
currentIndex: _currentPage,
|
||||
currentIndex: _controller.index,
|
||||
onTap: (int newIndex) {
|
||||
setState(() {
|
||||
_currentPage = newIndex;
|
||||
});
|
||||
_controller.index = newIndex;
|
||||
// Chain the user's original callback.
|
||||
if (widget.tabBar.onTap != null)
|
||||
widget.tabBar.onTap(newIndex);
|
||||
@ -273,6 +414,18 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Only dispose `_controller` when the state instance owns it.
|
||||
if (widget.controller == null) {
|
||||
_controller?.dispose();
|
||||
} else if (_controller?._isDisposed == false) {
|
||||
_controller.removeListener(_onCurrentIndexChange);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget laying out multiple tabs with only one active tab being built
|
||||
|
@ -10,11 +10,43 @@ import '../rendering/rendering_tester.dart';
|
||||
|
||||
List<int> selectedTabs;
|
||||
|
||||
class MockCupertinoTabController extends CupertinoTabController {
|
||||
MockCupertinoTabController({ int initialIndex }): super(initialIndex: initialIndex);
|
||||
|
||||
bool isDisposed = false;
|
||||
int numOfListeners = 0;
|
||||
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
numOfListeners++;
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
numOfListeners--;
|
||||
super.removeListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
selectedTabs = <int>[];
|
||||
});
|
||||
|
||||
BottomNavigationBarItem tabGenerator(int index) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: const ImageIcon(TestImageProvider(24, 24)),
|
||||
title: Text('Tab ${index + 1}'),
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('Tab switching', (WidgetTester tester) async {
|
||||
final List<int> tabsPainted = <int>[];
|
||||
|
||||
@ -203,7 +235,45 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Programmatic tab switching', (WidgetTester tester) async {
|
||||
testWidgets('Programmatic tab switching by changing the index of an existing controller', (WidgetTester tester) async {
|
||||
final CupertinoTabController controller = CupertinoTabController(initialIndex: 1);
|
||||
final List<int> tabsPainted = <int>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: _buildTabBar(),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
child: Text('Page ${index + 1}'),
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () { tabsPainted.add(index); }
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tabsPainted, <int>[1]);
|
||||
|
||||
controller.index = 0;
|
||||
await tester.pump();
|
||||
|
||||
expect(tabsPainted, <int>[1, 0]);
|
||||
// onTap is not called when changing tabs programmatically.
|
||||
expect(selectedTabs, isEmpty);
|
||||
|
||||
// Can still tap out of the programmatically selected tab.
|
||||
await tester.tap(find.text('Tab 2'));
|
||||
await tester.pump();
|
||||
|
||||
expect(tabsPainted, <int>[1, 0, 1]);
|
||||
expect(selectedTabs, <int>[1]);
|
||||
});
|
||||
|
||||
testWidgets('Programmatic tab switching by passing in a new controller', (WidgetTester tester) async {
|
||||
final List<int> tabsPainted = <int>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -227,7 +297,8 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: _buildTabBar(selectedTab: 1), // Programmatically change the tab now.
|
||||
tabBar: _buildTabBar(),
|
||||
controller: CupertinoTabController(initialIndex: 1), // Programmatically change the tab now.
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
child: Text('Page ${index + 1}'),
|
||||
@ -393,16 +464,9 @@ void main() {
|
||||
expect(MediaQuery.of(innerContext).padding.bottom, 0);
|
||||
});
|
||||
|
||||
testWidgets('Deleting tabs after selecting them works', (WidgetTester tester) async {
|
||||
testWidgets('Deleting tabs after selecting them should switch to the last available tab', (WidgetTester tester) async {
|
||||
final List<int> tabsBuilt = <int>[];
|
||||
|
||||
BottomNavigationBarItem tabGenerator(int index) {
|
||||
return BottomNavigationBarItem(
|
||||
icon: const ImageIcon(TestImageProvider(24, 24)),
|
||||
title: Text('Tab ${index + 1}'),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
@ -434,7 +498,7 @@ void main() {
|
||||
expect(find.text('Page 4'), findsOneWidget);
|
||||
tabsBuilt.clear();
|
||||
|
||||
// Delete 2 tabs.
|
||||
// Delete 2 tabs while Page 4 is still selected.
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
@ -448,7 +512,7 @@ void main() {
|
||||
return Text('Different page ${index + 1}');
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(tabsBuilt, <int>[0, 1]);
|
||||
@ -469,6 +533,314 @@ void main() {
|
||||
expect(find.text('Page 4', skipOffstage: false), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('If a controller is initially provided then the parent stops doing so for rebuilds, '
|
||||
'a new instance of CupertinoTabController should be created and used by the widget, '
|
||||
"while preserving the previous controller's tab index",
|
||||
(WidgetTester tester) async {
|
||||
final List<int> tabsPainted = <int>[];
|
||||
final CupertinoTabController oldController = CupertinoTabController(initialIndex: 0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(10, tabGenerator),
|
||||
),
|
||||
controller: oldController,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
child: Text('Page ${index + 1}'),
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () { tabsPainted.add(index); }
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(tabsPainted, <int> [0]);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(10, tabGenerator),
|
||||
),
|
||||
controller: null,
|
||||
tabBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
child: Text('Page ${index + 1}'),
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () { tabsPainted.add(index); }
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(tabsPainted, <int> [0, 0]);
|
||||
|
||||
await tester.tap(find.text('Tab 2'));
|
||||
await tester.pump();
|
||||
|
||||
// Tapping the tabs should still work.
|
||||
expect(tabsPainted, <int>[0, 0, 1]);
|
||||
|
||||
oldController.index = 10;
|
||||
await tester.pump();
|
||||
|
||||
// Changing [index] of the oldController should not work.
|
||||
expect(tabsPainted, <int> [0, 0, 1]);
|
||||
});
|
||||
|
||||
testWidgets('Do not call dispose on a controller that we do not own'
|
||||
'but do remove from its listeners when done listening to it',
|
||||
(WidgetTester tester) async {
|
||||
final MockCupertinoTabController mockController = MockCupertinoTabController(initialIndex: 0);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(2, tabGenerator),
|
||||
),
|
||||
controller: mockController,
|
||||
tabBuilder: (BuildContext context, int index) => const Placeholder(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(mockController.numOfListeners, 1);
|
||||
expect(mockController.isDisposed, isFalse);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(2, tabGenerator),
|
||||
),
|
||||
controller: null,
|
||||
tabBuilder: (BuildContext context, int index) => const Placeholder(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(mockController.numOfListeners, 0);
|
||||
expect(mockController.isDisposed, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('The owner can dispose the old controller', (WidgetTester tester) async {
|
||||
CupertinoTabController controller = CupertinoTabController(initialIndex: 2);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) => const Placeholder()
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(find.text('Tab 1'), findsOneWidget);
|
||||
expect(find.text('Tab 2'), findsOneWidget);
|
||||
expect(find.text('Tab 3'), findsOneWidget);
|
||||
|
||||
controller.dispose();
|
||||
controller = CupertinoTabController(initialIndex: 0);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(2, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) => const Placeholder()
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Should not crash here.
|
||||
expect(find.text('Tab 1'), findsOneWidget);
|
||||
expect(find.text('Tab 2'), findsOneWidget);
|
||||
expect(find.text('Tab 3'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('A controller can control more than one CupertinoTabScaffold,'
|
||||
'removal of listeners does not break the controller',
|
||||
(WidgetTester tester) async {
|
||||
final List<int> tabsPainted0 = <int>[];
|
||||
final List<int> tabsPainted1 = <int>[];
|
||||
MockCupertinoTabController controller = MockCupertinoTabController(initialIndex: 2);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () => tabsPainted0.add(index)
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () => tabsPainted1.add(index)
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(tabsPainted0, const <int>[2]);
|
||||
expect(tabsPainted1, const <int>[2]);
|
||||
expect(controller.numOfListeners, 2);
|
||||
|
||||
controller.index = 0;
|
||||
await tester.pump();
|
||||
expect(tabsPainted0, const <int>[2, 0]);
|
||||
expect(tabsPainted1, const <int>[2, 0]);
|
||||
|
||||
controller.index = 1;
|
||||
// Removing one of the tabs works.
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () => tabsPainted0.add(index)
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
expect(tabsPainted0, const <int>[2, 0, 1]);
|
||||
expect(tabsPainted1, const <int>[2, 0]);
|
||||
expect(controller.numOfListeners, 1);
|
||||
|
||||
// Replacing controller works.
|
||||
controller = MockCupertinoTabController(initialIndex: 2);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) {
|
||||
return CustomPaint(
|
||||
painter: TestCallbackPainter(
|
||||
onPaint: () => tabsPainted0.add(index)
|
||||
)
|
||||
);
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(tabsPainted0, const <int>[2, 0, 1, 2]);
|
||||
expect(tabsPainted1, const <int>[2, 0]);
|
||||
expect(controller.numOfListeners, 1);
|
||||
});
|
||||
|
||||
testWidgets('Assert when current tab index >= number of tabs', (WidgetTester tester) async {
|
||||
final CupertinoTabController controller = CupertinoTabController(initialIndex: 2);
|
||||
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(2, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) => Text('Different page ${index + 1}'),
|
||||
),
|
||||
)
|
||||
);
|
||||
} on AssertionError catch (e) {
|
||||
expect(e.toString(), contains('controller.index < tabBar.items.length'));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(3, tabGenerator),
|
||||
),
|
||||
controller: controller,
|
||||
tabBuilder: (BuildContext context, int index) => Text('Different page ${index + 1}'),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
expect(tester.takeException(), null);
|
||||
|
||||
controller.index = 10;
|
||||
await tester.pump();
|
||||
|
||||
final String message = tester.takeException().toString();
|
||||
expect(message, contains('current index ${controller.index}'));
|
||||
expect(message, contains('with 3 tabs'));
|
||||
});
|
||||
|
||||
testWidgets('Current tab index cannot go below zero or be null', (WidgetTester tester) async {
|
||||
void expectAssertionError(VoidCallback callback, String errorMessage) {
|
||||
try {
|
||||
callback();
|
||||
} on AssertionError catch (e) {
|
||||
expect(e.toString(), contains(errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
expectAssertionError(() => CupertinoTabController(initialIndex: -1), '>= 0');
|
||||
expectAssertionError(() => CupertinoTabController(initialIndex: null), '!= null');
|
||||
|
||||
final CupertinoTabController controller = CupertinoTabController();
|
||||
|
||||
expectAssertionError(() => controller.index = -1, '>= 0');
|
||||
expectAssertionError(() => controller.index = null, '!= null');
|
||||
});
|
||||
|
||||
testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async {
|
||||
// Regression testing for https://github.com/flutter/flutter/issues/28457.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user