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