Line data Source code
1 : import 'dart:collection'; 2 : import 'package:flutter/foundation.dart'; 3 : import 'package:flutter/widgets.dart'; 4 : 5 : import 'destination.dart'; 6 : import 'exceptions.dart'; 7 : import 'utils/utils.dart'; 8 : import 'widgets/index.dart'; 9 : 10 : /// A [NavigationController] manages the navigation state. 11 : /// 12 : /// Using the given [destinations] list, it maintains the navigation [stack]. 13 : /// 14 : /// The navigation stack is updated when a user navigates to specified destination 15 : /// by calling [goTo] method, or returns back with [goBack] method. 16 : /// 17 : /// The navigation controller (navigator) provides an access to a [currentDestination], 18 : /// which is one on the top of the stack. 19 : /// 20 : /// Initially, the navigation stack contains a destination at [initialDestinationIndex] 21 : /// in the provided list of destinations. 22 : /// 23 : /// [NavigationController] implements [ChangeNotifier] and notifies its listener when 24 : /// the [currentDestination]/[stack] is changed, or some error was happened. 25 : /// 26 : /// See also: 27 : /// - [Destination] 28 : /// - [NavigationScheme] 29 : /// - [NavigationControllerError] 30 : /// 31 : class NavigationController with ChangeNotifier { 32 : /// Creates navigation controller instance. 33 : /// 34 : /// Add initial destination to the navigation stack and creates a [GlobalKey] for 35 : /// a [Navigator] widget. 36 : /// 37 7 : NavigationController({ 38 : required this.destinations, 39 : this.builder = const DefaultNavigatorBuilder(), 40 : this.initialDestinationIndex = 0, 41 : this.notifyOnError = true, 42 : this.tag = '', 43 : }) { 44 35 : _stack.add(destinations[initialDestinationIndex]); 45 21 : key = GlobalKey<NavigatorState>(debugLabel: tag); 46 : } 47 : 48 : /// List of destinations, which this navigator operate of. 49 : /// 50 : final List<Destination> destinations; 51 : 52 : /// An implementation of [NavigatorBuilder] which creates a navigation UI. 53 : /// 54 : /// Defaults to [DefaultNavigatorBuilder] which uses Flutter's [Navigator] widget 55 : /// to represent the stack of destinations. 56 : /// 57 : /// Also the following implementations are available: 58 : /// - [BottomNavigationBuilder] allows to switch destination using Flutter's 59 : /// [BottomNavigationBar] widget. 60 : /// - [DrawerNavigationBuilder] uses drawer menu to navigate top-level destinations. 61 : /// - [TabsNavigationBuilder] uses [TabBar] with [TabBarView] widgets to switch destinations. 62 : /// 63 : /// You can implement your custom wrapper by extending the [NavigatorBuilder] class. 64 : /// 65 : /// See also: 66 : /// - [NavigatorBuilder] 67 : /// - [DefaultNavigatorBuilder] 68 : /// - [BottomNavigationBuilder] 69 : /// - [DrawerNavigationBuilder] 70 : /// - [TabsNavigationBuilder] 71 : /// 72 : final NavigatorBuilder builder; 73 : 74 : /// Index of the initial destination. 75 : /// 76 : /// Initial destination will be added to the navigation [stack] on creation the 77 : /// navigator. If it is omitted, the first destination in the [destinations] list 78 : /// will be used as initial one. 79 : /// 80 : final int initialDestinationIndex; 81 : 82 : /// Whether to notify listeners on errors in navigation actions. 83 : /// 84 : /// Defaults to true. Basically the [NavigationScheme] handles the errors. 85 : /// When set to false, the exception will be thrown on errors instead of notifying listeners. 86 : /// 87 : final bool notifyOnError; 88 : 89 : /// An identifier of this navigator. 90 : /// 91 : /// It is used in the debug logs to identify entries related to this navigator. 92 : /// 93 : final String? tag; 94 : 95 : /// Provides the global key for corresponding [Navigator] widget. 96 : /// 97 : late final GlobalKey<NavigatorState> key; 98 : 99 : Destination? _backFrom; 100 : 101 : /// The destination from [goBack] action is performed. 102 : /// 103 : /// It is set to current destination right before [goBack] action is processed. 104 : /// Otherwise it is set to null. 105 : /// 106 10 : Destination? get backFrom => _backFrom; 107 : 108 : NavigationControllerError? _error; 109 : 110 : /// Error details 111 : /// 112 10 : NavigationControllerError? get error => _error; 113 : 114 : /// Whether an error was happened on last [goTo] or [goBack] action. 115 : /// 116 8 : bool get hasError => _error != null; 117 : 118 : /// Indicates if persisting of navigation state in destination parameters is needed. 119 : /// 120 : /// When it is *true*, the following is happened on navigation to a destination: 121 : /// - If [DestinationSettings.reset] is not set in the requested destination, 122 : /// the current navigation state is saved in the requested destination parameters. 123 : /// Particularly, the [currentDestination] is saved in the [DestinationParameters.upwardParameterName] 124 : /// parameter, and current destination of each nested [NavigationController] 125 : /// is saved in the [DestinationParameters.nestedParameterName] parameter of the requested destination. 126 : /// - If [DestinationSettings.reset] is *true*, the navigation state is restored 127 : /// from the requested destination parameters. 128 : /// 129 5 : bool get keepStateInParameters => 130 15 : builder.keepStateInParameters == KeepingStateInParameters.always || 131 15 : builder.keepStateInParameters == KeepingStateInParameters.auto && kIsWeb; 132 : 133 : bool _shouldClose = false; 134 : 135 : /// Whether the navigator should close. 136 : /// 137 : /// It is set to 'true' when user call [goBack] method when the only destination 138 : /// is in the stack. 139 : /// 140 : /// If this is the root navigator in the [NavigationScheme], setting [shouldClose] 141 : /// to true will cause closing the app. 142 : /// 143 12 : bool get shouldClose => _shouldClose; 144 : 145 : final _stack = Queue<Destination>(); 146 : 147 20 : String get _tag => '$runtimeType::$tag'; 148 : 149 : /// The current destination of the navigator. 150 : /// 151 : /// It is the topmost destination in the navigation [stack]. 152 : /// 153 18 : Destination get currentDestination => _stack.last; 154 : 155 : /// The navigation [stack]. 156 : /// 157 : /// When [goTo] method is called, the destination is placed on the top of the stack, 158 : /// and when [goBack] method is called, the topmost destination is removed from 159 : /// the stack. 160 : /// 161 18 : List<Destination> get stack => _stack.toList(); 162 : 163 : /// Builds a widget that wraps destinations of the navigator. 164 : /// 165 3 : Widget build(BuildContext context) { 166 6 : return builder.build(context, this); 167 : } 168 : 169 : /// Opens specified destination. 170 : /// 171 : /// By calling calling this method, depending on [destination.settings], 172 : /// the given destination will be either added to the top of the navigation [stack], 173 : /// or will replace the topmost destination in the stack. 174 : /// 175 : /// Also, missing upward destinations can be added to the stack, if the 176 : /// current stack state doesn't match, and the [destination.upwardDestinationBuilder] 177 : /// is defined. 178 : /// 179 : /// Throws [UnknownDestinationException] if the navigator's [destinations] 180 : /// doesn't contain given destination. 181 : /// 182 5 : Future<void> goTo(Destination destination) async { 183 10 : Log.d(_tag, 184 15 : 'goTo(): destination=$destination, reset=${destination.settings.reset}'); 185 5 : _backFrom = null; 186 5 : _error = null; 187 5 : _shouldClose = false; 188 10 : if (currentDestination == destination) { 189 6 : if (!destination.settings.reset) { 190 6 : Log.d(_tag, 191 : 'goTo(): The destination is already on top. No action required.'); 192 3 : notifyListeners(); 193 : return; 194 : } 195 : } 196 5 : if (_isDestinationMatched(destination)) { 197 10 : await _updateStack(destination); 198 5 : notifyListeners(); 199 : } else { 200 2 : if (notifyOnError) { 201 4 : _error = NavigationControllerError(destination: destination); 202 2 : notifyListeners(); 203 : return; 204 : } else { 205 2 : throw UnknownDestinationException(destination); 206 : } 207 : } 208 : } 209 : 210 : /// Closes the current destination. 211 : /// 212 : /// The topmost destination is removed from the navigation [stack]. 213 : /// 214 : /// If it is the only destination in the stack, it remains in the stack and 215 : /// [shouldClose] flag is set to 'true'. 216 : /// 217 4 : void goBack() { 218 8 : _backFrom = currentDestination; 219 12 : if (_stack.length > 1) { 220 8 : _stack.removeLast(); 221 4 : _shouldClose = false; 222 : } else { 223 4 : _shouldClose = true; 224 : } 225 8 : Log.d(_tag, 226 16 : 'goBack(): destination=${_stack.last}, shouldClose=$_shouldClose'); 227 4 : notifyListeners(); 228 : } 229 : 230 2 : void resetStack(List<Destination> destinations) { 231 4 : _stack.clear(); 232 4 : for (final destination in destinations) { 233 4 : _stack.add(destination); 234 : } 235 : } 236 : 237 5 : bool _isDestinationMatched(Destination destination) => 238 25 : destinations.any((element) => element.isMatch(destination.uri)); 239 : 240 5 : Future<void> _updateStack(Destination destination) async { 241 10 : if (destination.settings.reset) { 242 6 : _stack.clear(); 243 : } else { 244 15 : if (destination.settings.action == DestinationAction.replace) { 245 4 : _stack.removeLast(); 246 : } 247 : } 248 10 : final upwardStack = await _buildUpwardStack(destination); 249 5 : if (upwardStack.isNotEmpty) { 250 : // Find first missing item of upward stack 251 : int startUpwardFrom = 0; 252 9 : for (int i = 0; i < upwardStack.length; i++) { 253 14 : if (_stack.isNotEmpty && _stack.last == upwardStack[i]) { 254 2 : startUpwardFrom = i + 1; 255 : } 256 : } 257 : // Add all missing upward destinations to the stack 258 6 : if (startUpwardFrom < upwardStack.length) { 259 9 : for (int i = startUpwardFrom; i < upwardStack.length; i++) { 260 9 : _stack.addLast(upwardStack[i]); 261 : } 262 : } 263 : } 264 10 : _stack.addLast(destination); 265 : } 266 : 267 5 : Future<List<Destination>> _buildUpwardStack(Destination destination) async { 268 5 : final result = <Destination>[]; 269 10 : var upwardDestination = await destination.upwardDestination; 270 : while (upwardDestination != null) { 271 3 : result.insert(0, upwardDestination); 272 6 : upwardDestination = await upwardDestination.upwardDestination; 273 : } 274 : return result; 275 : } 276 : } 277 : 278 : /// Automatic persisting of navigation state. 279 : /// 280 : /// Once persisting of navigation state in destination parameters is enabled, 281 : /// the current stack will be serialized and saved in the [DestinationParameters.stateParameterName] 282 : /// parameter on navigation to a destination. 283 : /// When the destination with persisted navigation state is requested by the platform, 284 : /// the navigation stack will be deserialized from the parameter and explicitly set in the 285 : /// navigation controller. 286 : /// 287 : /// Basically, persisting of navigation state in destination parameters make sense in web apps, 288 : /// to be able to restore arbitrary navigation stack when the user navigates to a destination 289 : /// through the browser history or a deeplink. 290 : /// To support this, the [auto] option is used in [NavigatorBuilder] by default. 291 : /// 292 : /// When automatic persisting of navigation state is disabled, 293 : /// you still able to implement your custom logic manually, by providing proper [Destination.upwardDestinationBuilder]. 294 : /// 295 11 : enum KeepingStateInParameters { 296 : /// The navigation state will be always kept 297 : /// 298 : always, 299 : 300 : /// The navigation state will be only kept when the app is running on the Web platform. 301 : /// 302 : auto, 303 : 304 : /// The navigation state will not be kept automatically. 305 : /// 306 : none, 307 : } 308 : 309 : /// Contains navigation error details 310 : /// 311 : class NavigationControllerError { 312 : /// Creates an error object 313 2 : NavigationControllerError({ 314 : this.destination, 315 : }); 316 : 317 : /// A destination related to this error 318 : /// 319 : final Destination? destination; 320 : 321 2 : @override 322 6 : String toString() => '$runtimeType: destination=$destination'; 323 : }