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 6 : NavigationController({ 37 : required this.destinations, 38 : this.builder = const DefaultNavigatorBuilder(), 39 : this.initialDestinationIndex = 0, 40 : this.notifyOnError = true, 41 : this.tag = '', 42 : }) { 43 30 : _stack.add(destinations[initialDestinationIndex]); 44 18 : 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] that creates a wrapping widget tree 52 : /// around destinations. 53 : /// 54 : /// Defaults to [DefaultNavigatorBuilder] that wraps destinations to Flutter's 55 : /// [Navigator] widget. 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 : NavigationControllerError? _error; 100 : 101 : /// Error details 102 : /// 103 10 : NavigationControllerError? get error => _error; 104 : 105 : /// Whether an error was happened on [goTo()] or [goBack()] actions. 106 : /// 107 8 : bool get hasError => _error != null; 108 : 109 : bool _gotBack = false; 110 : 111 : /// Whether the back action was performed. 112 : /// 113 : /// It is 'true' if the last navigation method call was the [goBack()]. 114 : /// 115 10 : bool get gotBack => _gotBack; 116 : 117 : bool _shouldClose = false; 118 : 119 : /// Whether the navigator should close. 120 : /// 121 : /// It is set to 'true' when user call [onBack] method when the only destination 122 : /// is in the stack. 123 : /// If this is the root navigator in the [NavigationScheme], setting [shouldClose] 124 : /// to true will cause closing the app. 125 : /// 126 12 : bool get shouldClose => _shouldClose; 127 : 128 : final _stack = Queue<Destination>(); 129 : 130 20 : String get _tag => '$runtimeType::$tag'; 131 : 132 : /// The current destination of the navigator. 133 : /// 134 : /// It is the top destination in the navigation [stack]. 135 : /// 136 18 : Destination get currentDestination => _stack.last; 137 : 138 : /// The navigation [stack]. 139 : /// 140 : /// When [goTo] method is called, the destination is added to the stack, 141 : /// and when [goBack] method is called, the [currentDestination] is removed from 142 : /// the stack. 143 : /// 144 18 : List<Destination> get stack => _stack.toList(); 145 : 146 : /// Builds a widget that wraps the destination's content. 147 : /// 148 2 : Widget build(BuildContext context) { 149 4 : return builder.build(context, this); 150 : } 151 : 152 : /// Opens specified destination. 153 : /// 154 : /// By calling calling this method, depending on [destination.configuration], 155 : /// the given destination will be either added to the navigation [stack], or 156 : /// will replace the current destination. 157 : /// 158 : /// Also, missing upward destinations can be added to the stack, if the 159 : /// current stack state doesn't match, and the [destination.upwardDestinationBuilder] 160 : /// is defined. This mostly could happen when it is navigated as a deeplink. 161 : /// 162 : /// Throws [UnknownDestinationException] if the navigator's [destinations] 163 : /// doesn't contain given destination. 164 : /// 165 5 : Future<void> goTo(Destination destination) async { 166 10 : Log.d(_tag, 167 15 : 'goTo(): destination=$destination, reset=${destination.configuration.reset}'); 168 5 : _error = null; 169 5 : _gotBack = false; 170 5 : _shouldClose = false; 171 10 : if (currentDestination == destination) { 172 8 : if (!destination.configuration.reset) { 173 8 : Log.d(_tag, 174 : 'goTo(): The destination is already on top. No action required.'); 175 4 : notifyListeners(); 176 : return; 177 : } 178 : } 179 5 : if (_isDestinationMatched(destination)) { 180 5 : _updateStack(destination); 181 5 : notifyListeners(); 182 : } else { 183 2 : if (notifyOnError) { 184 4 : _error = NavigationControllerError(destination: destination); 185 2 : notifyListeners(); 186 : return; 187 : } else { 188 2 : throw UnknownDestinationException(destination); 189 : } 190 : } 191 : } 192 : 193 : /// Closes the current destination. 194 : /// 195 : /// The current destination is removed from the navigation [stack]. 196 : /// 197 : /// If it is the only destination in the stack, it remains in the stack and 198 : /// [shouldClose] flag is set to 'true'. 199 : /// 200 4 : void goBack() { 201 4 : _gotBack = true; 202 12 : if (_stack.length > 1) { 203 8 : _stack.removeLast(); 204 4 : _shouldClose = false; 205 : } else { 206 4 : _shouldClose = true; 207 : } 208 8 : Log.d(_tag, 209 16 : 'goBack(): destination=${_stack.last}, shouldClose=$_shouldClose'); 210 4 : notifyListeners(); 211 : } 212 : 213 5 : bool _isDestinationMatched(Destination destination) => 214 25 : destinations.any((element) => element.isMatch(destination.uri)); 215 : 216 5 : void _updateStack(Destination destination) { 217 10 : if (destination.configuration.reset) { 218 4 : _stack.clear(); 219 : } else { 220 15 : if (destination.configuration.action == DestinationAction.replace) { 221 0 : _stack.removeLast(); 222 : } 223 : } 224 5 : final upwardStack = _buildUpwardStack(destination); 225 5 : if (upwardStack.isNotEmpty) { 226 : // Find first missing item of upward stack 227 : int startUpwardFrom = 0; 228 6 : for (int i = 0; i < upwardStack.length; i++) { 229 4 : if (_stack.isNotEmpty && _stack.last == upwardStack[i]) { 230 0 : startUpwardFrom = i + 1; 231 : } 232 : } 233 : // Add all missing upward destinations to the stack 234 4 : if (startUpwardFrom < upwardStack.length) { 235 6 : for (int i = startUpwardFrom; i < upwardStack.length; i++) { 236 6 : _stack.addLast(upwardStack[i]); 237 : } 238 : } 239 : } 240 10 : _stack.addLast(destination); 241 : } 242 : 243 5 : List<Destination> _buildUpwardStack(Destination destination) { 244 5 : final result = <Destination>[]; 245 5 : var upwardDestination = destination.upwardDestination; 246 : while (upwardDestination != null) { 247 2 : result.insert(0, upwardDestination); 248 2 : upwardDestination = upwardDestination.upwardDestination; 249 : } 250 : return result; 251 : } 252 : } 253 : 254 : /// Contains navigation error details 255 : /// 256 : class NavigationControllerError { 257 : /// Creates an error object 258 2 : NavigationControllerError({ 259 : this.destination, 260 : }); 261 : 262 : /// A destination related to this error 263 : /// 264 : final Destination? destination; 265 : 266 0 : @override 267 0 : String toString() => '$runtimeType={destination: $destination}'; 268 : }