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 'bottom_tab_bar.dart';
|
||||||
import 'theme.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.
|
/// 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
|
/// 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
|
/// will automatically listen to the provided [CupertinoTabBar]'s tap callbacks
|
||||||
/// to change the active tab.
|
/// 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
|
/// Tabs' contents are built with the provided [tabBuilder] at the active
|
||||||
/// tab index. The [tabBuilder] must be able to build the same number of
|
/// 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]
|
/// pages as there are [tabBar.items]. Inactive tabs will be moved [Offstage]
|
||||||
@ -87,6 +187,7 @@ import 'theme.dart';
|
|||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [CupertinoTabBar], the bottom tab bar inserted in the scaffold.
|
/// * [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
|
/// * [CupertinoTabView], the typical root content of each tab that holds its own
|
||||||
/// [Navigator] stack.
|
/// [Navigator] stack.
|
||||||
/// * [CupertinoPageRoute], a route hosting modal pages with iOS style transitions.
|
/// * [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.
|
/// Creates a layout for applications with a tab bar at the bottom.
|
||||||
///
|
///
|
||||||
/// The [tabBar] and [tabBuilder] arguments must not be null.
|
/// The [tabBar] and [tabBuilder] arguments must not be null.
|
||||||
const CupertinoTabScaffold({
|
CupertinoTabScaffold({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.tabBar,
|
@required this.tabBar,
|
||||||
@required this.tabBuilder,
|
@required this.tabBuilder,
|
||||||
|
this.controller,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.resizeToAvoidBottomInset = true,
|
this.resizeToAvoidBottomInset = true,
|
||||||
}) : assert(tabBar != null),
|
}) : assert(tabBar != null),
|
||||||
assert(tabBuilder != 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);
|
super(key: key);
|
||||||
|
|
||||||
/// The [tabBar] is a [CupertinoTabBar] drawn at the bottom of the screen
|
/// 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
|
/// that lets the user switch between different tabs in the main content area
|
||||||
/// when present.
|
/// when present.
|
||||||
///
|
///
|
||||||
/// Setting and changing [CupertinoTabBar.currentIndex] programmatically will
|
/// The [CupertinoTabBar.currentIndex] is only used to initialize a
|
||||||
/// change the currently selected tab item in the [tabBar] as well as change
|
/// [CupertinoTabController] when no [controller] is provided. Subsequently
|
||||||
/// the currently focused tab from the [tabBuilder].
|
/// 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.
|
/// If [CupertinoTabBar.onTap] is provided, it will still be called.
|
||||||
/// [CupertinoTabScaffold] automatically also listen to the
|
/// [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
|
/// and change the actively displayed tab in [CupertinoTabScaffold]'s own
|
||||||
/// main content area.
|
/// main content area.
|
||||||
///
|
///
|
||||||
@ -126,6 +235,14 @@ class CupertinoTabScaffold extends StatefulWidget {
|
|||||||
/// Must not be null.
|
/// Must not be null.
|
||||||
final CupertinoTabBar tabBar;
|
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.
|
/// An [IndexedWidgetBuilder] that's called when tabs become active.
|
||||||
///
|
///
|
||||||
/// The widgets built by [IndexedWidgetBuilder] is typically a [CupertinoTabView]
|
/// The widgets built by [IndexedWidgetBuilder] is typically a [CupertinoTabView]
|
||||||
@ -162,29 +279,55 @@ class CupertinoTabScaffold extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||||
int _currentPage;
|
CupertinoTabController _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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
|
@override
|
||||||
void didUpdateWidget(CupertinoTabScaffold oldWidget) {
|
void didUpdateWidget(CupertinoTabScaffold oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (_currentPage >= widget.tabBar.items.length) {
|
if (widget.controller != oldWidget.controller) {
|
||||||
// Clip down to an acceptable range.
|
_updateTabController(shouldDisposeOldController: oldWidget.controller == null);
|
||||||
_currentPage = widget.tabBar.items.length - 1;
|
} else if (_controller.index >= widget.tabBar.items.length) {
|
||||||
// Sanity check, since CupertinoTabBar.items's minimum length is 2.
|
// If a new [tabBar] with less than (_controller.index + 1) items is provided,
|
||||||
assert(
|
// clamp the current index.
|
||||||
_currentPage >= 0,
|
_controller.index = widget.tabBar.items.length - 1;
|
||||||
'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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +339,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
|||||||
MediaQueryData newMediaQuery = MediaQuery.of(context);
|
MediaQueryData newMediaQuery = MediaQuery.of(context);
|
||||||
|
|
||||||
Widget content = _TabSwitchingView(
|
Widget content = _TabSwitchingView(
|
||||||
currentTabIndex: _currentPage,
|
currentTabIndex: _controller.index,
|
||||||
tabNumber: widget.tabBar.items.length,
|
tabNumber: widget.tabBar.items.length,
|
||||||
tabBuilder: widget.tabBuilder,
|
tabBuilder: widget.tabBuilder,
|
||||||
);
|
);
|
||||||
@ -248,14 +391,12 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
|||||||
stacked.add(Align(
|
stacked.add(Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
// Override the tab bar's currentIndex to the current tab and hook in
|
// 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.
|
// provided callback.
|
||||||
child: widget.tabBar.copyWith(
|
child: widget.tabBar.copyWith(
|
||||||
currentIndex: _currentPage,
|
currentIndex: _controller.index,
|
||||||
onTap: (int newIndex) {
|
onTap: (int newIndex) {
|
||||||
setState(() {
|
_controller.index = newIndex;
|
||||||
_currentPage = newIndex;
|
|
||||||
});
|
|
||||||
// Chain the user's original callback.
|
// Chain the user's original callback.
|
||||||
if (widget.tabBar.onTap != null)
|
if (widget.tabBar.onTap != null)
|
||||||
widget.tabBar.onTap(newIndex);
|
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
|
/// 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;
|
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() {
|
void main() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
selectedTabs = <int>[];
|
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 {
|
testWidgets('Tab switching', (WidgetTester tester) async {
|
||||||
final List<int> tabsPainted = <int>[];
|
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>[];
|
final List<int> tabsPainted = <int>[];
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -227,7 +297,8 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
CupertinoApp(
|
CupertinoApp(
|
||||||
home: CupertinoTabScaffold(
|
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) {
|
tabBuilder: (BuildContext context, int index) {
|
||||||
return CustomPaint(
|
return CustomPaint(
|
||||||
child: Text('Page ${index + 1}'),
|
child: Text('Page ${index + 1}'),
|
||||||
@ -393,16 +464,9 @@ void main() {
|
|||||||
expect(MediaQuery.of(innerContext).padding.bottom, 0);
|
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>[];
|
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(
|
await tester.pumpWidget(
|
||||||
CupertinoApp(
|
CupertinoApp(
|
||||||
home: CupertinoTabScaffold(
|
home: CupertinoTabScaffold(
|
||||||
@ -434,7 +498,7 @@ void main() {
|
|||||||
expect(find.text('Page 4'), findsOneWidget);
|
expect(find.text('Page 4'), findsOneWidget);
|
||||||
tabsBuilt.clear();
|
tabsBuilt.clear();
|
||||||
|
|
||||||
// Delete 2 tabs.
|
// Delete 2 tabs while Page 4 is still selected.
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
CupertinoApp(
|
CupertinoApp(
|
||||||
home: CupertinoTabScaffold(
|
home: CupertinoTabScaffold(
|
||||||
@ -448,7 +512,7 @@ void main() {
|
|||||||
return Text('Different page ${index + 1}');
|
return Text('Different page ${index + 1}');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(tabsBuilt, <int>[0, 1]);
|
expect(tabsBuilt, <int>[0, 1]);
|
||||||
@ -469,6 +533,314 @@ void main() {
|
|||||||
expect(find.text('Page 4', skipOffstage: false), findsNothing);
|
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 {
|
testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async {
|
||||||
// Regression testing for https://github.com/flutter/flutter/issues/28457.
|
// Regression testing for https://github.com/flutter/flutter/issues/28457.
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user