• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/widgets.dart';
6
7import 'app.dart' show CupertinoApp;
8import 'route.dart';
9
10/// A single tab view with its own [Navigator] state and history.
11///
12/// A typical tab view used as the content of each tab in a [CupertinoTabScaffold]
13/// where multiple tabs with parallel navigation states and history can
14/// co-exist.
15///
16/// [CupertinoTabView] configures the top-level [Navigator] to search for routes
17/// in the following order:
18///
19///  1. For the `/` route, the [builder] property, if non-null, is used.
20///
21///  2. Otherwise, the [routes] table is used, if it has an entry for the route,
22///     including `/` if [builder] is not specified.
23///
24///  3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
25///     non-null value for any _valid_ route not handled by [builder] and [routes].
26///
27///  4. Finally if all else fails [onUnknownRoute] is called.
28///
29/// These navigation properties are not shared with any sibling [CupertinoTabView]
30/// nor any ancestor or descendant [Navigator] instances.
31///
32/// To push a route above this [CupertinoTabView] instead of inside it (such
33/// as when showing a dialog on top of all tabs), use
34/// `Navigator.of(rootNavigator: true)`.
35///
36/// See also:
37///
38///  * [CupertinoTabScaffold], a typical host that supports switching between tabs.
39///  * [CupertinoPageRoute], a typical modal page route pushed onto the
40///    [CupertinoTabView]'s [Navigator].
41class 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    this.routes,
49    this.onGenerateRoute,
50    this.onUnknownRoute,
51    this.navigatorObservers = const <NavigatorObserver>[],
52  }) : assert(navigatorObservers != null),
53       super(key: key);
54
55  /// The widget builder for the default route of the tab view
56  /// ([Navigator.defaultRouteName], which is `/`).
57  ///
58  /// If a [builder] is specified, then [routes] must not include an entry for `/`,
59  /// as [builder] takes its place.
60  ///
61  /// Rebuilding a [CupertinoTabView] with a different [builder] will not clear
62  /// its current navigation stack or update its descendant. Instead, trigger a
63  /// rebuild from a descendant in its subtree. This can be done via methods such
64  /// as:
65  ///
66  ///  * Calling [State.setState] on a descendant [StatefulWidget]'s [State]
67  ///  * Modifying an [InheritedWidget] that a descendant registered itself
68  ///    as a dependent to.
69  final WidgetBuilder builder;
70
71  /// A key to use when building this widget's [Navigator].
72  ///
73  /// If a [navigatorKey] is specified, the [Navigator] can be directly
74  /// manipulated without first obtaining it from a [BuildContext] via
75  /// [Navigator.of]: from the [navigatorKey], use the [GlobalKey.currentState]
76  /// getter.
77  ///
78  /// If this is changed, a new [Navigator] will be created, losing all the
79  /// tab's state in the process; in that case, the [navigatorObservers]
80  /// must also be changed, since the previous observers will be attached to the
81  /// previous navigator.
82  final GlobalKey<NavigatorState> navigatorKey;
83
84  /// The title of the default route.
85  final String defaultTitle;
86
87  /// This tab view's routing table.
88  ///
89  /// When a named route is pushed with [Navigator.pushNamed] inside this tab view,
90  /// the route name is looked up in this map. If the name is present,
91  /// the associated [WidgetBuilder] is used to construct a [CupertinoPageRoute]
92  /// that performs an appropriate transition to the new route.
93  ///
94  /// If the tab view only has one page, then you can specify it using [builder] instead.
95  ///
96  /// If [builder] is specified, then it implies an entry in this table for the
97  /// [Navigator.defaultRouteName] route (`/`), and it is an error to
98  /// redundantly provide such a route in the [routes] table.
99  ///
100  /// If a route is requested that is not specified in this table (or by
101  /// [builder]), then the [onGenerateRoute] callback is called to build the page
102  /// instead.
103  ///
104  /// This routing table is not shared with any routing tables of ancestor or
105  /// descendant [Navigator]s.
106  final Map<String, WidgetBuilder> routes;
107
108  /// The route generator callback used when the tab view is navigated to a named route.
109  ///
110  /// This is used if [routes] does not contain the requested route.
111  final RouteFactory onGenerateRoute;
112
113  /// Called when [onGenerateRoute] also fails to generate a route.
114  ///
115  /// This callback is typically used for error handling. For example, this
116  /// callback might always generate a "not found" page that describes the route
117  /// that wasn't found.
118  ///
119  /// The default implementation pushes a route that displays an ugly error
120  /// message.
121  final RouteFactory onUnknownRoute;
122
123  /// The list of observers for the [Navigator] created in this tab view.
124  ///
125  /// This list of observers is not shared with ancestor or descendant [Navigator]s.
126  final List<NavigatorObserver> navigatorObservers;
127
128  @override
129  _CupertinoTabViewState createState() {
130    return _CupertinoTabViewState();
131  }
132}
133
134class _CupertinoTabViewState extends State<CupertinoTabView> {
135  HeroController _heroController;
136  List<NavigatorObserver> _navigatorObservers;
137
138  @override
139  void initState() {
140    super.initState();
141    _heroController = CupertinoApp.createCupertinoHeroController();
142    _updateObservers();
143  }
144
145  @override
146  void didUpdateWidget(CupertinoTabView oldWidget) {
147    super.didUpdateWidget(oldWidget);
148    if (widget.navigatorKey != oldWidget.navigatorKey
149        || widget.navigatorObservers != oldWidget.navigatorObservers) {
150      _updateObservers();
151    }
152  }
153
154  void _updateObservers() {
155    _navigatorObservers =
156        List<NavigatorObserver>.from(widget.navigatorObservers)
157          ..add(_heroController);
158  }
159
160  @override
161  Widget build(BuildContext context) {
162    return Navigator(
163      key: widget.navigatorKey,
164      onGenerateRoute: _onGenerateRoute,
165      onUnknownRoute: _onUnknownRoute,
166      observers: _navigatorObservers,
167    );
168  }
169
170  Route<dynamic> _onGenerateRoute(RouteSettings settings) {
171    final String name = settings.name;
172    WidgetBuilder routeBuilder;
173    String title;
174    if (name == Navigator.defaultRouteName && widget.builder != null) {
175      routeBuilder = widget.builder;
176      title = widget.defaultTitle;
177    } else if (widget.routes != null) {
178      routeBuilder = widget.routes[name];
179    }
180    if (routeBuilder != null) {
181      return CupertinoPageRoute<dynamic>(
182        builder: routeBuilder,
183        title: title,
184        settings: settings,
185      );
186    }
187    if (widget.onGenerateRoute != null)
188      return widget.onGenerateRoute(settings);
189    return null;
190  }
191
192  Route<dynamic> _onUnknownRoute(RouteSettings settings) {
193    assert(() {
194      if (widget.onUnknownRoute == null) {
195        throw FlutterError(
196          'Could not find a generator for route $settings in the $runtimeType.\n'
197          'Generators for routes are searched for in the following order:\n'
198          ' 1. For the "/" route, the "builder" property, if non-null, is used.\n'
199          ' 2. Otherwise, the "routes" table is used, if it has an entry for '
200          'the route.\n'
201          ' 3. Otherwise, onGenerateRoute is called. It should return a '
202          'non-null value for any valid route not handled by "builder" and "routes".\n'
203          ' 4. Finally if all else fails onUnknownRoute is called.\n'
204          'Unfortunately, onUnknownRoute was not set.'
205        );
206      }
207      return true;
208    }());
209    final Route<dynamic> result = widget.onUnknownRoute(settings);
210    assert(() {
211      if (result == null) {
212        throw FlutterError(
213          'The onUnknownRoute callback returned null.\n'
214          'When the $runtimeType requested the route $settings from its '
215          'onUnknownRoute callback, the callback returned null. Such callbacks '
216          'must never return null.'
217        );
218      }
219      return true;
220    }());
221    return result;
222  }
223}
224