blob: 8728196eee9b8c574df35e931e0df7982f85aa7f [file] [log] [blame]
// 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({
this.navigatorObservers = const <NavigatorObserver>[],
/// 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;
State<CupertinoTabView> createState() => _CupertinoTabViewState();
class _CupertinoTabViewState extends State<CupertinoTabView> {
late HeroController _heroController;
late List<NavigatorObserver> _navigatorObservers;
void initState() {
_heroController = CupertinoApp.createCupertinoHeroController();
void didUpdateWidget(CupertinoTabView oldWidget) {
if (widget.navigatorKey != oldWidget.navigatorKey
|| widget.navigatorObservers != oldWidget.navigatorObservers) {
void _updateObservers() {
_navigatorObservers =
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 =;
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;