Make CupertinoTabScaffold restorable (#67770)
This commit is contained in:
parent
daa6b2cc29
commit
053ebf2c08
@ -61,6 +61,8 @@ import 'theme.dart';
|
||||
///
|
||||
/// * [CupertinoTabScaffold], a tabbed application root layout that can be
|
||||
/// controlled by a [CupertinoTabController].
|
||||
/// * [RestorableCupertinoTabController], which is a restorable version
|
||||
/// of this controller.
|
||||
class CupertinoTabController extends ChangeNotifier {
|
||||
/// Creates a [CupertinoTabController] to control the tab index of [CupertinoTabScaffold]
|
||||
/// and [CupertinoTabBar].
|
||||
@ -211,6 +213,7 @@ class CupertinoTabScaffold extends StatefulWidget {
|
||||
this.controller,
|
||||
this.backgroundColor,
|
||||
this.resizeToAvoidBottomInset = true,
|
||||
this.restorationId,
|
||||
}) : assert(tabBar != null),
|
||||
assert(tabBuilder != null),
|
||||
assert(
|
||||
@ -289,12 +292,46 @@ class CupertinoTabScaffold extends StatefulWidget {
|
||||
/// Defaults to true and cannot be null.
|
||||
final bool resizeToAvoidBottomInset;
|
||||
|
||||
/// Restoration ID to save and restore the state of the [CupertinoTabScaffold].
|
||||
///
|
||||
/// This property only has an effect when no [controller] has been provided:
|
||||
/// If it is non-null (and no [controller] has been provided), the scaffold
|
||||
/// will persist and restore the currently selected tab index. If a
|
||||
/// [controller] has been provided, it is the responsibility of the owner of
|
||||
/// that controller to persist and restore it, e.g. by using a
|
||||
/// [RestorableCupertinoTabController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RestorationManager], which explains how state restoration works in
|
||||
/// Flutter.
|
||||
final String? restorationId;
|
||||
|
||||
@override
|
||||
_CupertinoTabScaffoldState createState() => _CupertinoTabScaffoldState();
|
||||
}
|
||||
|
||||
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
CupertinoTabController? _controller;
|
||||
class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> with RestorationMixin {
|
||||
RestorableCupertinoTabController? _internalController;
|
||||
CupertinoTabController get _controller => widget.controller ?? _internalController!.value;
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
_restoreInternalController();
|
||||
}
|
||||
|
||||
void _restoreInternalController() {
|
||||
if (_internalController != null) {
|
||||
registerForRestoration(_internalController!, 'controller');
|
||||
_internalController!.value.addListener(_onCurrentIndexChange);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -302,30 +339,33 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
_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;
|
||||
void _updateTabController([CupertinoTabController? oldWidgetController]) {
|
||||
if (widget.controller == null && _internalController == null) {
|
||||
// No widget-provided controller: create an internal controller.
|
||||
_internalController = RestorableCupertinoTabController(initialIndex: widget.tabBar.currentIndex);
|
||||
if (!restorePending) {
|
||||
_restoreInternalController(); // Also adds the listener to the controller.
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisposeOldController) {
|
||||
_controller?.dispose();
|
||||
} else if (_controller?._isDisposed == false) {
|
||||
_controller!.removeListener(_onCurrentIndexChange);
|
||||
if (widget.controller != null && _internalController != null) {
|
||||
// Use the widget-provided controller.
|
||||
unregisterFromRestoration(_internalController!);
|
||||
_internalController!.dispose();
|
||||
_internalController = null;
|
||||
}
|
||||
if (oldWidgetController != widget.controller) {
|
||||
// The widget-provided controller has changed: move listeners.
|
||||
if (oldWidgetController?._isDisposed == false) {
|
||||
oldWidgetController!.removeListener(_onCurrentIndexChange);
|
||||
}
|
||||
widget.controller?.addListener(_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 "
|
||||
_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'
|
||||
);
|
||||
|
||||
@ -338,11 +378,11 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
void didUpdateWidget(CupertinoTabScaffold oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController(shouldDisposeOldController: oldWidget.controller == null);
|
||||
} else if (_controller!.index >= widget.tabBar.items.length) {
|
||||
_updateTabController(oldWidget.controller);
|
||||
} 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;
|
||||
_controller.index = widget.tabBar.items.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,7 +392,7 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
MediaQueryData newMediaQuery = MediaQuery.of(context)!;
|
||||
|
||||
Widget content = _TabSwitchingView(
|
||||
currentTabIndex: _controller!.index,
|
||||
currentTabIndex: _controller.index,
|
||||
tabCount: widget.tabBar.items.length,
|
||||
tabBuilder: widget.tabBuilder,
|
||||
);
|
||||
@ -415,9 +455,9 @@ class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
|
||||
// our own listener to update the [_controller.currentIndex] on top of a possibly user
|
||||
// provided callback.
|
||||
child: widget.tabBar.copyWith(
|
||||
currentIndex: _controller!.index,
|
||||
currentIndex: _controller.index,
|
||||
onTap: (int newIndex) {
|
||||
_controller!.index = newIndex;
|
||||
_controller.index = newIndex;
|
||||
// Chain the user's original callback.
|
||||
widget.tabBar.onTap?.call(newIndex);
|
||||
},
|
||||
@ -431,13 +471,10 @@ 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);
|
||||
if (widget.controller?._isDisposed == false) {
|
||||
_controller.removeListener(_onCurrentIndexChange);
|
||||
}
|
||||
|
||||
_internalController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -555,3 +592,39 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [RestorableProperty] that knows how to store and restore a
|
||||
/// [CupertinoTabController].
|
||||
///
|
||||
/// The [CupertinoTabController] is accessible via the [value] getter. During
|
||||
/// state restoration, the property will restore [CupertinoTabController.index]
|
||||
/// to the value it had when the restoration data it is getting restored from
|
||||
/// was collected.
|
||||
class RestorableCupertinoTabController extends RestorableChangeNotifier<CupertinoTabController> {
|
||||
/// Creates a [RestorableCupertinoTabController] 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.
|
||||
RestorableCupertinoTabController({ int initialIndex = 0 })
|
||||
: assert(initialIndex != null),
|
||||
assert(initialIndex >= 0),
|
||||
_initialIndex = initialIndex;
|
||||
|
||||
final int _initialIndex;
|
||||
|
||||
@override
|
||||
CupertinoTabController createDefaultValue() {
|
||||
return CupertinoTabController(initialIndex: _initialIndex);
|
||||
}
|
||||
|
||||
@override
|
||||
CupertinoTabController fromPrimitives(Object data) {
|
||||
return CupertinoTabController(initialIndex: data as int);
|
||||
}
|
||||
|
||||
@override
|
||||
Object? toPrimitives() {
|
||||
return value.index;
|
||||
}
|
||||
}
|
||||
|
@ -292,6 +292,43 @@ abstract class RestorableListenable<T extends Listenable> extends RestorableProp
|
||||
}
|
||||
}
|
||||
|
||||
/// A base class for creating a [RestorableProperty] that stores and restores a
|
||||
/// [ChangeNotifier].
|
||||
///
|
||||
/// This class may be used to implement a [RestorableProperty] for a
|
||||
/// [ChangeNotifier], whose information it needs to store in the restoration
|
||||
/// data change whenever the [ChangeNotifier] notifies its listeners.
|
||||
///
|
||||
/// The [RestorationMixin] this property is registered with will call
|
||||
/// [toPrimitives] whenever the wrapped [ChangeNotifier] notifies its listeners
|
||||
/// to update the information that this property has stored in the restoration
|
||||
/// data.
|
||||
///
|
||||
/// Furthermore, the property will dispose the wrapped [ChangeNotifier] when
|
||||
/// either the property itself is disposed or its value is replaced with another
|
||||
/// [ChangeNotifier] instance.
|
||||
abstract class RestorableChangeNotifier<T extends ChangeNotifier> extends RestorableListenable<T> {
|
||||
@override
|
||||
void initWithValue(T value) {
|
||||
_diposeOldValue();
|
||||
super.initWithValue(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_diposeOldValue();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _diposeOldValue() {
|
||||
if (_value != null) {
|
||||
// Scheduling a microtask for dispose to give other entities a chance
|
||||
// to remove their listeners first.
|
||||
scheduleMicrotask(_value!.dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [RestorableProperty] that knows how to store and restore a
|
||||
/// [TextEditingController].
|
||||
///
|
||||
@ -299,7 +336,7 @@ abstract class RestorableListenable<T extends Listenable> extends RestorableProp
|
||||
/// state restoration, the property will restore [TextEditingController.text] to
|
||||
/// the value it had when the restoration data it is getting restored from was
|
||||
/// collected.
|
||||
class RestorableTextEditingController extends RestorableListenable<TextEditingController> {
|
||||
class RestorableTextEditingController extends RestorableChangeNotifier<TextEditingController> {
|
||||
/// Creates a [RestorableTextEditingController].
|
||||
///
|
||||
/// This constructor treats a null `text` argument as if it were the empty
|
||||
@ -331,27 +368,4 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo
|
||||
Object toPrimitives() {
|
||||
return value.text;
|
||||
}
|
||||
|
||||
TextEditingController? _controller;
|
||||
|
||||
@override
|
||||
void initWithValue(TextEditingController value) {
|
||||
_disposeControllerIfNecessary();
|
||||
_controller = value;
|
||||
super.initWithValue(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_disposeControllerIfNecessary();
|
||||
}
|
||||
|
||||
void _disposeControllerIfNecessary() {
|
||||
if (_controller != null) {
|
||||
// Scheduling a microtask for dispose to give other entities a chance
|
||||
// to remove their listeners first.
|
||||
scheduleMicrotask(_controller!.dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1110,6 +1110,110 @@ void main() {
|
||||
expect(contents.length, greaterThan(0));
|
||||
expect(contents.any((RichText t) => t.textScaleFactor != 99), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('state restoration', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
restorationScopeId: 'app',
|
||||
home: CupertinoTabScaffold(
|
||||
restorationId: 'scaffold',
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(
|
||||
4,
|
||||
(int i) => BottomNavigationBarItem(icon: const Icon(CupertinoIcons.map), label: 'Tab $i'),
|
||||
),
|
||||
),
|
||||
tabBuilder: (BuildContext context, int i) => Text('Content $i'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Content 0'), findsOneWidget);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsNothing);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Tab 2'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsOneWidget);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
await tester.restartAndRestore();
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsOneWidget);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
final TestRestorationData data = await tester.getRestorationData();
|
||||
|
||||
await tester.tap(find.text('Tab 1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsOneWidget);
|
||||
expect(find.text('Content 2'), findsNothing);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
await tester.restoreFrom(data);
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsOneWidget);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('switch from internal to external controller with state restoration', (WidgetTester tester) async {
|
||||
Widget buildWidget({CupertinoTabController? controller}) {
|
||||
return CupertinoApp(
|
||||
restorationScopeId: 'app',
|
||||
home: CupertinoTabScaffold(
|
||||
controller: controller,
|
||||
restorationId: 'scaffold',
|
||||
tabBar: CupertinoTabBar(
|
||||
items: List<BottomNavigationBarItem>.generate(
|
||||
4,
|
||||
(int i) => BottomNavigationBarItem(icon: const Icon(CupertinoIcons.map), label: 'Tab $i'),
|
||||
),
|
||||
),
|
||||
tabBuilder: (BuildContext context, int i) => Text('Content $i'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildWidget());
|
||||
|
||||
expect(find.text('Content 0'), findsOneWidget);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsNothing);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('Tab 2'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsOneWidget);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
|
||||
final CupertinoTabController controller = CupertinoTabController(initialIndex: 3);
|
||||
await tester.pumpWidget(buildWidget(controller: controller));
|
||||
|
||||
expect(find.text('Content 0'), findsNothing);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsNothing);
|
||||
expect(find.text('Content 3'), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(buildWidget());
|
||||
|
||||
expect(find.text('Content 0'), findsOneWidget);
|
||||
expect(find.text('Content 1'), findsNothing);
|
||||
expect(find.text('Content 2'), findsNothing);
|
||||
expect(find.text('Content 3'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user