| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/widgets.dart'; |
| |
| import 'app.dart' show CupertinoApp; |
| import 'route.dart'; |
| |
| /// A single tab view with its own [Navigator] state and history. |
| /// |
| /// A typical tab view is used as the content of each tab in a |
| /// [CupertinoTabScaffold] where multiple tabs with parallel navigation states |
| /// and history can co-exist. |
| /// |
| /// [CupertinoTabView] configures the top-level [Navigator] to search for routes |
| /// in the following order: |
| /// |
| /// 1. For the `/` route, the [builder] property, if non-null, is used. |
| /// |
| /// 2. Otherwise, the [routes] table is used, if it has an entry for the route, |
| /// including `/` if [builder] is not specified. |
| /// |
| /// 3. Otherwise, [onGenerateRoute] is called, if provided. It should return a |
| /// non-null value for any _valid_ route not handled by [builder] and [routes]. |
| /// |
| /// 4. Finally if all else fails [onUnknownRoute] is called. |
| /// |
| /// These navigation properties are not shared with any sibling [CupertinoTabView] |
| /// nor any ancestor or descendant [Navigator] instances. |
| /// |
| /// To push a route above this [CupertinoTabView] instead of inside it (such |
| /// as when showing a dialog on top of all tabs), use |
| /// `Navigator.of(rootNavigator: true)`. |
| /// |
| /// See also: |
| /// |
| /// * [CupertinoTabScaffold], a typical host that supports switching between tabs. |
| /// * [CupertinoPageRoute], a typical modal page route pushed onto the |
| /// [CupertinoTabView]'s [Navigator]. |
| class CupertinoTabView extends StatefulWidget { |
| /// Creates the content area for a tab in a [CupertinoTabScaffold]. |
| const CupertinoTabView({ |
| super.key, |
| this.builder, |
| this.navigatorKey, |
| this.defaultTitle, |
| this.routes, |
| this.onGenerateRoute, |
| this.onUnknownRoute, |
| this.navigatorObservers = const <NavigatorObserver>[], |
| this.restorationScopeId, |
| }); |
| |
| /// The widget builder for the default route of the tab view |
| /// ([Navigator.defaultRouteName], which is `/`). |
| /// |
| /// If a [builder] is specified, then [routes] must not include an entry for `/`, |
| /// as [builder] takes its place. |
| /// |
| /// Rebuilding a [CupertinoTabView] with a different [builder] will not clear |
| /// its current navigation stack or update its descendant. Instead, trigger a |
| /// rebuild from a descendant in its subtree. This can be done via methods such |
| /// as: |
| /// |
| /// * Calling [State.setState] on a descendant [StatefulWidget]'s [State] |
| /// * Modifying an [InheritedWidget] that a descendant registered itself |
| /// as a dependent to. |
| final WidgetBuilder? builder; |
| |
| /// A key to use when building this widget's [Navigator]. |
| /// |
| /// If a [navigatorKey] is specified, the [Navigator] can be directly |
| /// manipulated without first obtaining it from a [BuildContext] via |
| /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState] |
| /// getter. |
| /// |
| /// If this is changed, a new [Navigator] will be created, losing all the |
| /// tab's state in the process; in that case, the [navigatorObservers] |
| /// must also be changed, since the previous observers will be attached to the |
| /// previous navigator. |
| final GlobalKey<NavigatorState>? navigatorKey; |
| |
| /// The title of the default route. |
| final String? defaultTitle; |
| |
| /// This tab view's routing table. |
| /// |
| /// When a named route is pushed with [Navigator.pushNamed] inside this tab view, |
| /// the route name is looked up in this map. If the name is present, |
| /// the associated [widgets.WidgetBuilder] is used to construct a |
| /// [CupertinoPageRoute] that performs an appropriate transition to the new |
| /// route. |
| /// |
| /// If the tab view only has one page, then you can specify it using [builder] instead. |
| /// |
| /// If [builder] is specified, then it implies an entry in this table for the |
| /// [Navigator.defaultRouteName] route (`/`), and it is an error to |
| /// redundantly provide such a route in the [routes] table. |
| /// |
| /// If a route is requested that is not specified in this table (or by |
| /// [builder]), then the [onGenerateRoute] callback is called to build the page |
| /// instead. |
| /// |
| /// This routing table is not shared with any routing tables of ancestor or |
| /// descendant [Navigator]s. |
| final Map<String, WidgetBuilder>? routes; |
| |
| /// The route generator callback used when the tab view is navigated to a named route. |
| /// |
| /// This is used if [routes] does not contain the requested route. |
| final RouteFactory? onGenerateRoute; |
| |
| /// Called when [onGenerateRoute] also fails to generate a route. |
| /// |
| /// This callback is typically used for error handling. For example, this |
| /// callback might always generate a "not found" page that describes the route |
| /// that wasn't found. |
| /// |
| /// The default implementation pushes a route that displays an ugly error |
| /// message. |
| final RouteFactory? onUnknownRoute; |
| |
| /// The list of observers for the [Navigator] created in this tab view. |
| /// |
| /// This list of observers is not shared with ancestor or descendant [Navigator]s. |
| final List<NavigatorObserver> navigatorObservers; |
| |
| /// Restoration ID to save and restore the state of the [Navigator] built by |
| /// this [CupertinoTabView]. |
| /// |
| /// {@macro flutter.widgets.navigator.restorationScopeId} |
| final String? restorationScopeId; |
| |
| @override |
| State<CupertinoTabView> createState() => _CupertinoTabViewState(); |
| } |
| |
| class _CupertinoTabViewState extends State<CupertinoTabView> { |
| late HeroController _heroController; |
| late List<NavigatorObserver> _navigatorObservers; |
| |
| @override |
| void initState() { |
| super.initState(); |
| _heroController = CupertinoApp.createCupertinoHeroController(); |
| _updateObservers(); |
| } |
| |
| @override |
| void didUpdateWidget(CupertinoTabView oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.navigatorKey != oldWidget.navigatorKey |
| || widget.navigatorObservers != oldWidget.navigatorObservers) { |
| _updateObservers(); |
| } |
| } |
| |
| void _updateObservers() { |
| _navigatorObservers = |
| List<NavigatorObserver>.of(widget.navigatorObservers) |
| ..add(_heroController); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return Navigator( |
| key: widget.navigatorKey, |
| onGenerateRoute: _onGenerateRoute, |
| onUnknownRoute: _onUnknownRoute, |
| observers: _navigatorObservers, |
| restorationScopeId: widget.restorationScopeId, |
| ); |
| } |
| |
| Route<dynamic>? _onGenerateRoute(RouteSettings settings) { |
| final String? name = settings.name; |
| final WidgetBuilder? routeBuilder; |
| String? title; |
| if (name == Navigator.defaultRouteName && widget.builder != null) { |
| routeBuilder = widget.builder; |
| title = widget.defaultTitle; |
| } else { |
| routeBuilder = widget.routes?[name]; |
| } |
| if (routeBuilder != null) { |
| return CupertinoPageRoute<dynamic>( |
| builder: routeBuilder, |
| title: title, |
| settings: settings, |
| ); |
| } |
| return widget.onGenerateRoute?.call(settings); |
| } |
| |
| Route<dynamic>? _onUnknownRoute(RouteSettings settings) { |
| assert(() { |
| if (widget.onUnknownRoute == null) { |
| throw FlutterError( |
| 'Could not find a generator for route $settings in the $runtimeType.\n' |
| 'Generators for routes are searched for in the following order:\n' |
| ' 1. For the "/" route, the "builder" property, if non-null, is used.\n' |
| ' 2. Otherwise, the "routes" table is used, if it has an entry for ' |
| 'the route.\n' |
| ' 3. Otherwise, onGenerateRoute is called. It should return a ' |
| 'non-null value for any valid route not handled by "builder" and "routes".\n' |
| ' 4. Finally if all else fails onUnknownRoute is called.\n' |
| 'Unfortunately, onUnknownRoute was not set.', |
| ); |
| } |
| return true; |
| }()); |
| final Route<dynamic>? result = widget.onUnknownRoute!(settings); |
| assert(() { |
| if (result == null) { |
| throw FlutterError( |
| 'The onUnknownRoute callback returned null.\n' |
| 'When the $runtimeType requested the route $settings from its ' |
| 'onUnknownRoute callback, the callback returned null. Such callbacks ' |
| 'must never return null.', |
| ); |
| } |
| return true; |
| }()); |
| return result; |
| } |
| } |