tab_view.dart 8.35 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// 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';

7
import 'app.dart' show CupertinoApp;
8 9 10 11
import 'route.dart';

/// A single tab view with its own [Navigator] state and history.
///
12 13 14
/// 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.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
///
/// [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]
Ian Hickson's avatar
Ian Hickson committed
30
/// nor any ancestor or descendant [Navigator] instances.
31
///
32 33 34 35
/// 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)`.
///
36 37
/// See also:
///
38 39 40
///  * [CupertinoTabScaffold], a typical host that supports switching between tabs.
///  * [CupertinoPageRoute], a typical modal page route pushed onto the
///    [CupertinoTabView]'s [Navigator].
41
class CupertinoTabView extends StatefulWidget {
42
  /// Creates the content area for a tab in a [CupertinoTabScaffold].
43
  const CupertinoTabView({
44
    Key? key,
45
    this.builder,
46
    this.navigatorKey,
47
    this.defaultTitle,
48 49 50
    this.routes,
    this.onGenerateRoute,
    this.onUnknownRoute,
51
    this.navigatorObservers = const <NavigatorObserver>[],
52
    this.restorationScopeId,
53 54 55 56 57 58 59 60
  }) : assert(navigatorObservers != null),
       super(key: key);

  /// 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.
61 62 63 64 65 66 67 68 69
  ///
  /// 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.
70
  final WidgetBuilder? builder;
71

72 73 74 75 76 77 78 79 80 81 82
  /// 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.
83
  final GlobalKey<NavigatorState>? navigatorKey;
84

85
  /// The title of the default route.
86
  final String? defaultTitle;
87

88 89 90 91
  /// 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,
Janice Collins's avatar
Janice Collins committed
92 93 94
  /// the associated [widgets.WidgetBuilder] is used to construct a
  /// [CupertinoPageRoute] that performs an appropriate transition to the new
  /// route.
95 96 97 98 99 100 101 102 103 104 105 106
  ///
  /// 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
Ian Hickson's avatar
Ian Hickson committed
107
  /// descendant [Navigator]s.
108
  final Map<String, WidgetBuilder>? routes;
109 110 111 112

  /// 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.
113
  final RouteFactory? onGenerateRoute;
114 115 116 117 118 119 120 121 122

  /// 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.
123
  final RouteFactory? onUnknownRoute;
124 125 126

  /// The list of observers for the [Navigator] created in this tab view.
  ///
Ian Hickson's avatar
Ian Hickson committed
127
  /// This list of observers is not shared with ancestor or descendant [Navigator]s.
128 129
  final List<NavigatorObserver> navigatorObservers;

130 131 132 133 134 135
  /// Restoration ID to save and restore the state of the [Navigator] built by
  /// this [CupertinoTabView].
  ///
  /// {@macro flutter.widgets.navigator.restorationScopeId}
  final String? restorationScopeId;

136
  @override
137
  State<CupertinoTabView> createState() => _CupertinoTabViewState();
138 139 140
}

class _CupertinoTabViewState extends State<CupertinoTabView> {
141 142
  late HeroController _heroController;
  late List<NavigatorObserver> _navigatorObservers;
143 144 145 146

  @override
  void initState() {
    super.initState();
147
    _heroController = CupertinoApp.createCupertinoHeroController();
148 149 150 151
    _updateObservers();
  }

  @override
152 153 154 155 156 157
  void didUpdateWidget(CupertinoTabView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.navigatorKey != oldWidget.navigatorKey
        || widget.navigatorObservers != oldWidget.navigatorObservers) {
      _updateObservers();
    }
158 159 160 161
  }

  void _updateObservers() {
    _navigatorObservers =
162
        List<NavigatorObserver>.from(widget.navigatorObservers)
163 164 165
          ..add(_heroController);
  }

166 167
  @override
  Widget build(BuildContext context) {
168
    return Navigator(
169
      key: widget.navigatorKey,
170 171
      onGenerateRoute: _onGenerateRoute,
      onUnknownRoute: _onUnknownRoute,
172
      observers: _navigatorObservers,
173
      restorationScopeId: widget.restorationScopeId,
174 175 176
    );
  }

177 178 179 180
  Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
    final String? name = settings.name;
    WidgetBuilder? routeBuilder;
    String? title;
181
    if (name == Navigator.defaultRouteName && widget.builder != null) {
182
      routeBuilder = widget.builder;
183
      title = widget.defaultTitle;
184
    } else if (widget.routes != null) {
185
      routeBuilder = widget.routes![name];
186
    }
187
    if (routeBuilder != null) {
188
      return CupertinoPageRoute<dynamic>(
189
        builder: routeBuilder,
190
        title: title,
191 192 193
        settings: settings,
      );
    }
194
    if (widget.onGenerateRoute != null)
195
      return widget.onGenerateRoute!(settings);
196 197 198
    return null;
  }

199
  Route<dynamic>? _onUnknownRoute(RouteSettings settings) {
200
    assert(() {
201
      if (widget.onUnknownRoute == null) {
202 203 204 205 206 207 208 209 210
        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'
211
          'Unfortunately, onUnknownRoute was not set.',
212
        );
213 214
      }
      return true;
215
    }());
216
    final Route<dynamic>? result = widget.onUnknownRoute!(settings);
217 218
    assert(() {
      if (result == null) {
219 220 221 222
        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 '
223
          'must never return null.',
224
        );
225 226
      }
      return true;
227
    }());
228 229
    return result;
  }
230
}