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