Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 :
4 : import 'package:collection/collection.dart';
5 : import 'package:flutter/foundation.dart';
6 : import 'package:flutter/widgets.dart';
7 :
8 : import 'destination.dart';
9 : import 'exceptions.dart';
10 : import 'navigation_controller.dart';
11 : import 'router_delegate.dart';
12 : import 'route_parser.dart';
13 : import 'utils/utils.dart';
14 :
15 : /// Defines a navigation scheme of the app.
16 : ///
17 : /// Contains a list of possible [destinations] to navigate in the app.
18 : /// Each destination can be a final, i.e. is directly rendered as a some screen,
19 : /// or might include a nested navigator with its own destinations.
20 : ///
21 : /// Until the custom [navigator] is provided, the NavigatorScheme creates a default
22 : /// root navigator, that manages top level destinations.
23 : ///
24 : /// In case of some navigation error, user will be redirected to [errorDestination],
25 : /// if it is specified. Otherwise an exception will be thrown.
26 : ///
27 : /// See also:
28 : /// - [Destination]
29 : /// - [NavigationController]
30 : ///
31 : class NavigationScheme with ChangeNotifier {
32 : /// Creates navigation scheme.
33 : ///
34 5 : NavigationScheme({
35 : List<Destination> destinations = const <Destination>[],
36 : this.errorDestination,
37 : this.waitingOverlayBuilder,
38 : NavigationController? navigator,
39 : }) : assert(
40 12 : (destinations.isEmpty ? navigator!.destinations : destinations)
41 15 : .any((destination) => destination.isHome),
42 : 'One of destinations must be a home destination.'),
43 : assert(
44 5 : errorDestination == null ||
45 : navigator == null ||
46 2 : (navigator.destinations
47 6 : .any((destination) => destination == errorDestination)),
48 : 'When "errorDestination" and custom "navigator" are specified, you must include the "errorDestination" to the "navigator"s destinations') {
49 5 : _rootNavigator = navigator ??
50 5 : NavigationController(
51 5 : destinations: <Destination>[
52 : ...destinations,
53 15 : if (errorDestination != null) errorDestination!,
54 : ],
55 : tag: 'Root',
56 : );
57 10 : _routerDelegate = TheseusRouterDelegate(navigationScheme: this);
58 10 : _routeParser = TheseusRouteInformationParser(navigationScheme: this);
59 15 : _currentDestination = _rootNavigator.currentDestination;
60 10 : _initializeNavigator(_rootNavigator);
61 5 : _updateCurrentDestination(backFrom: null);
62 : }
63 :
64 : /// The destination to redirect in case of error.
65 : ///
66 : final Destination? errorDestination;
67 :
68 : /// Returns a widget to display while destination is resolving.
69 : ///
70 : /// Resolving the destination might be asynchronous, for example, because of parsing typed
71 : /// parameters or checking redirection conditions.
72 : ///
73 : /// In these cases this function is used to build a widget, which would be displayed
74 : /// until the current destination is resolved.
75 : ///
76 : final Widget Function(BuildContext context, Destination destination)?
77 : waitingOverlayBuilder;
78 :
79 : late Destination _currentDestination;
80 :
81 : /// The current destination within the whole navigation scheme.
82 : ///
83 : /// To get a current top level destination, use [rootNavigator.currentDestination].
84 : ///
85 8 : Destination get currentDestination => _currentDestination;
86 :
87 : bool _isResolving = false;
88 :
89 : /// Indicates if a current destination is in resolving state.
90 : ///
91 : /// This flag is turned on during performing of the redirection validations, or
92 : /// parsing of typed parameters.
93 : ///
94 4 : bool get isResolving => _isResolving;
95 :
96 : final _navigatorListeners = <NavigationController, VoidCallback?>{};
97 :
98 : final _navigatorMatches = <Destination, NavigationController>{};
99 :
100 : final _navigatorOwners = <NavigationController, Destination>{};
101 :
102 : final _destinationCompleters = <Destination, Completer<void>>{};
103 :
104 : late final NavigationController _rootNavigator;
105 :
106 : /// The root navigator in the navigation scheme.
107 : ///
108 : /// This navigator manages top level destinations.
109 : ///
110 6 : NavigationController get rootNavigator => _rootNavigator;
111 :
112 : late final TheseusRouterDelegate _routerDelegate;
113 :
114 : /// Reference to the RouterDelegate implementation
115 : ///
116 6 : TheseusRouterDelegate get routerDelegate => _routerDelegate;
117 :
118 : late final TheseusRouteInformationParser _routeParser;
119 :
120 : /// Reference to the RouteInformationParser implementation
121 : ///
122 4 : TheseusRouteInformationParser get routeParser => _routeParser;
123 :
124 : /// Stores the original destination in case of redirection.
125 : ///
126 2 : Destination? get redirectedFrom =>
127 6 : _currentDestination.settings.redirectedFrom;
128 :
129 : bool _shouldClose = false;
130 :
131 : /// Whether the app should close.
132 : ///
133 : /// This flag is set on when user perform 'Back' action on top most destination.
134 : ///
135 8 : bool get shouldClose => _shouldClose;
136 :
137 2 : @override
138 : void dispose() {
139 4 : _routerDelegate.dispose();
140 2 : _removeNavigatorListeners();
141 2 : super.dispose();
142 : }
143 :
144 : /// Find a destination in the scheme that match a given URI.
145 : ///
146 : /// Returns 'null' if no destination matching the URI was found.
147 : ///
148 15 : Destination? findDestination(String uri) => _navigatorMatches.keys
149 15 : .firstWhereOrNull((destination) => destination.isMatch(uri));
150 :
151 : /// Returns a proper navigator in the navigation scheme for a given destination.
152 : ///
153 : /// Returns 'null' if no navigator found.
154 : ///
155 4 : NavigationController? findNavigator(Destination destination) =>
156 16 : _navigatorMatches[findDestination(destination.path)];
157 :
158 : /// Navigates to specified [destination].
159 : ///
160 : /// First, searches the navigation scheme for proper navigator for the destination.
161 : /// If found, uses the navigator's [goTo] method to navigate to the destination.
162 : /// Otherwise throws [UnknownDestinationException].
163 : ///
164 4 : Future<void> goTo(Destination destination) async {
165 8 : if (currentDestination == destination) {
166 4 : Log.d(runtimeType,
167 2 : 'goTo(): Ignore navigation to $destination. It is already the current destination.');
168 : return;
169 : }
170 :
171 4 : final navigator = findNavigator(destination);
172 : if (navigator == null) {
173 2 : _handleError(destination);
174 2 : return SynchronousFuture(null);
175 : }
176 8 : Log.d(runtimeType,
177 20 : 'goTo(): navigator=${navigator.tag}, destination=$destination, redirectedFrom=${destination.settings.redirectedFrom}, currentDestination=$currentDestination');
178 4 : _shouldClose = false;
179 :
180 4 : final completer = Completer<void>();
181 4 : _setupCompleter(destination, completer);
182 :
183 4 : if (navigator.keepStateInParameters &&
184 4 : navigator.stack.isNotEmpty &&
185 4 : navigator.currentDestination != destination &&
186 4 : destination.settings.reset &&
187 2 : _hasStateInParameters(destination)) {
188 4 : Log.d(runtimeType, 'goTo(): Restore navigation state');
189 2 : _restoreStateFromParameters(destination, navigator);
190 : } else {
191 4 : navigator.goTo(destination);
192 : }
193 :
194 4 : return completer.future;
195 : }
196 :
197 : /// Close the current destination.
198 : ///
199 : /// If the current destination is the last one, this requests closing the app by
200 : /// setting the [shouldClose] flag.
201 : ///
202 3 : void goBack() {
203 6 : final navigator = findNavigator(_currentDestination);
204 : if (navigator == null) {
205 0 : _handleError(_currentDestination);
206 : return;
207 : }
208 3 : navigator.goBack();
209 : }
210 :
211 : /// Resolves the current destination
212 : ///
213 : /// Applies redirection validations to the current destination.
214 : /// While validations are performed, the [isResolving] flag is set to true.
215 : /// This allows to display a widget returned by [waitingOverlayBuilder]
216 : /// until the destination is resolved.
217 : ///
218 : /// In case of validation are not passed, redirects to corresponding redirection destination.
219 : ///
220 4 : Future<void> resolve() async {
221 4 : Timer isResolvingTimer = Timer(
222 : const Duration(milliseconds: 500),
223 2 : () {
224 2 : if (!_isResolving) {
225 2 : _isResolving = true;
226 2 : notifyListeners();
227 : }
228 : },
229 : );
230 :
231 : final destinationsToComplete = <Destination>{};
232 :
233 8 : final navigator = findNavigator(_currentDestination);
234 : if (navigator == null) {
235 0 : _handleError(_currentDestination);
236 0 : return SynchronousFuture(null);
237 : }
238 4 : if (navigator.keepStateInParameters &&
239 6 : (!_currentDestination.settings.reset ||
240 4 : !_hasStateInParameters(_currentDestination))) {
241 4 : Log.d(runtimeType, 'resolve(): Save navigation state');
242 4 : destinationsToComplete.add(_currentDestination);
243 8 : _currentDestination = await _saveStateInParameters(_currentDestination);
244 : }
245 :
246 4 : final requestedDestination = _currentDestination;
247 4 : destinationsToComplete.add(requestedDestination);
248 :
249 8 : final resolvedDestination = await _resolveDestination(requestedDestination);
250 4 : isResolvingTimer.cancel();
251 8 : Log.d(runtimeType,
252 8 : 'resolve(): requestedDestination=$requestedDestination, resolvedDestination=$resolvedDestination, currentDestination=$_currentDestination');
253 8 : if (requestedDestination != _currentDestination) {
254 2 : _isResolving = false;
255 2 : notifyListeners();
256 : return;
257 : }
258 4 : if (resolvedDestination == requestedDestination) {
259 4 : _isResolving = false;
260 8 : for (var destination in destinationsToComplete) {
261 4 : _completeResolvedDestination(destination);
262 : }
263 4 : notifyListeners();
264 : return;
265 : }
266 6 : goTo(resolvedDestination.withSettings(resolvedDestination.settings
267 2 : .copyWith(redirectedFrom: requestedDestination)));
268 : }
269 :
270 5 : void _initializeNavigator(NavigationController navigator) {
271 8 : void listener() => _onNavigatorStateChanged(navigator);
272 :
273 : // Add a listener of the navigator
274 10 : _navigatorListeners[navigator] = listener;
275 5 : navigator.addListener(listener);
276 :
277 10 : for (var destination in navigator.destinations) {
278 10 : _navigatorMatches[destination] = navigator;
279 5 : if (!destination.isFinalDestination) {
280 : // Set navigation owner
281 9 : _navigatorOwners[destination.navigator!] = destination;
282 : // Initialize nested navigator
283 6 : _initializeNavigator(destination.navigator!);
284 : }
285 : }
286 : }
287 :
288 2 : void _handleError(Destination? destination) {
289 2 : if (errorDestination != null) {
290 6 : goTo((errorDestination!).withSettings(
291 6 : errorDestination!.settings.copyWith(redirectedFrom: destination)));
292 : } else {
293 2 : throw UnknownDestinationException(destination);
294 : }
295 : }
296 :
297 4 : void _onNavigatorStateChanged(NavigationController navigator) {
298 8 : Log.d(runtimeType,
299 20 : 'onNavigatorStateChanged(): navigator=${navigator.tag}, error=${navigator.error}, backFrom=${navigator.backFrom}, shouldClose=${navigator.shouldClose}');
300 4 : if (navigator.hasError) {
301 0 : _handleError(navigator.error!.destination);
302 : }
303 8 : final owner = _navigatorOwners[navigator];
304 : if (owner != null) {
305 6 : Log.d(runtimeType, 'onNavigatorStateChanged(): owner=$owner');
306 2 : if (navigator.backFrom != null) {
307 0 : if (navigator.shouldClose) {
308 0 : final parentNavigator = findNavigator(owner);
309 : if (parentNavigator == null) {
310 0 : _handleError(owner);
311 : return;
312 : }
313 0 : parentNavigator.goBack();
314 : } else {
315 0 : _updateCurrentDestination(backFrom: navigator.backFrom);
316 : }
317 : } else {
318 6 : if (navigator.currentDestination.settings.reset) {
319 0 : goTo(owner.withSettings(owner.settings.copyWith(reset: true)));
320 : } else {
321 2 : goTo(owner);
322 : }
323 : }
324 : } else {
325 8 : _updateCurrentDestination(backFrom: navigator.backFrom);
326 : }
327 : }
328 :
329 2 : void _removeNavigatorListeners() {
330 6 : for (var navigator in _navigatorListeners.keys) {
331 6 : navigator.removeListener(_navigatorListeners[navigator]!);
332 : }
333 : }
334 :
335 4 : void _setupCompleter(Destination destination, Completer completer) {
336 : var destinationToComplete = destination;
337 8 : _destinationCompleters[destinationToComplete] = completer;
338 : // Setup the same completer for nested destinations,
339 : // if they don't have their own non-completed ones
340 4 : while (!destinationToComplete.isFinalDestination) {
341 : destinationToComplete =
342 4 : destinationToComplete.navigator!.currentDestination;
343 6 : if (_destinationCompleters[destinationToComplete]?.isCompleted ?? true) {
344 4 : _destinationCompleters[destinationToComplete] = completer;
345 : }
346 : }
347 : }
348 :
349 5 : void _updateCurrentDestination({required Destination? backFrom}) {
350 : // TODO: Probably '_shouldClose' variable is not needed, we can use '_rootNavigator' directly
351 15 : _shouldClose = _rootNavigator.shouldClose;
352 5 : if (_shouldClose) {
353 6 : Log.d(runtimeType,
354 9 : 'updateCurrentDestination(): currentDestination=$_currentDestination, shouldClose=$_shouldClose');
355 3 : notifyListeners();
356 : return;
357 : }
358 :
359 10 : Destination newDestination = _rootNavigator.currentDestination;
360 5 : while (!newDestination.isFinalDestination) {
361 4 : newDestination = newDestination.navigator!.currentDestination;
362 : }
363 10 : Log.d(runtimeType,
364 10 : 'updateCurrentDestination(): currentDestination=$_currentDestination, newDestination=$newDestination');
365 10 : if (_currentDestination != newDestination ||
366 10 : newDestination.settings.reset) {
367 4 : _currentDestination = newDestination;
368 14 : if (_currentDestination == backFrom?.settings.redirectedFrom) {
369 2 : notifyListeners();
370 : return;
371 : }
372 4 : resolve();
373 : }
374 : }
375 :
376 2 : Future<Destination> _saveStateInParameters(Destination destination) async {
377 2 : Future<List<String>> getCleanUris(List<Destination> stack) async {
378 2 : final result = <String>[];
379 2 : for (final destination in stack.where(
380 8 : (destination) => destination.settings.redirectedFrom == null)) {
381 8 : result.add((await _removeStateFromParameters(destination)).uri);
382 : }
383 : return result;
384 : }
385 :
386 2 : final stateMap = <String, List<String>>{
387 8 : '/': await getCleanUris(_rootNavigator.stack),
388 : };
389 4 : for (final navigator in _navigatorOwners.keys) {
390 0 : if (navigator.keepStateInParameters) {
391 0 : stateMap[_navigatorOwners[navigator]!.path] =
392 0 : await getCleanUris(navigator.stack);
393 : }
394 : }
395 2 : final rawParametersWithState = <String, String>{
396 2 : DestinationParameters.stateParameterName: jsonEncode(stateMap)
397 : };
398 : rawParametersWithState
399 6 : .addAll(destination.parameters?.map ?? const <String, String>{});
400 4 : final parametersWithState = await destination.parser
401 2 : .parametersFromMap(rawParametersWithState);
402 2 : return destination.withParameters(parametersWithState);
403 : }
404 :
405 2 : bool _hasStateInParameters(Destination destination) =>
406 4 : destination.parameters?.map
407 2 : .containsKey(DestinationParameters.stateParameterName) ??
408 : false;
409 :
410 2 : Future<Destination> _removeStateFromParameters(
411 : Destination destination) async =>
412 6 : destination.withParameters(await destination.parser
413 4 : .parametersFromMap((Map.from(
414 4 : destination.parameters?.map ?? const <String, String>{}))
415 2 : ..remove(DestinationParameters.stateParameterName)));
416 :
417 2 : Future<void> _restoreStateFromParameters(
418 : Destination destination, NavigationController navigator) async {
419 : final stateValue =
420 6 : destination.parameters?.map[DestinationParameters.stateParameterName];
421 : if (stateValue == null) {
422 0 : navigator.goTo(destination);
423 : return;
424 : }
425 :
426 2 : final stateMap = jsonDecode(stateValue);
427 :
428 4 : for (final key in stateMap.keys) {
429 2 : final eventualNavigator = key == '/'
430 2 : ? _rootNavigator
431 0 : : (await _routeParser
432 0 : .parseRouteInformation(RouteInformation(location: key)))
433 0 : .navigator!;
434 2 : final destinations = <Destination>[];
435 4 : for (final uri in stateMap[key]) {
436 6 : destinations.add(await _routeParser
437 4 : .parseRouteInformation(RouteInformation(location: uri)));
438 : }
439 2 : eventualNavigator.resetStack(destinations);
440 : }
441 2 : _updateCurrentDestination(backFrom: null);
442 : }
443 :
444 4 : Future<Destination> _resolveDestination(Destination destination) async {
445 : // Check redirections that are defined for given destination
446 7 : for (var redirection in destination.redirections) {
447 6 : if (!(await redirection.validate(destination))) {
448 6 : return await _resolveDestination(redirection.destination);
449 : }
450 : }
451 : // In case of nested destination, validate the owner
452 4 : final navigator = findNavigator(destination);
453 : if (navigator == null) {
454 0 : throw UnknownDestinationException(destination);
455 : }
456 8 : final owner = _navigatorOwners[navigator];
457 : if (owner == null) {
458 : return destination;
459 : }
460 4 : final resolvedOwner = await _resolveDestination(owner);
461 2 : return owner != resolvedOwner ? resolvedOwner : destination;
462 : }
463 :
464 4 : void _completeResolvedDestination(Destination destination) {
465 : Destination? destinationToComplete = destination;
466 : while (destinationToComplete != null) {
467 12 : if (!(_destinationCompleters[destinationToComplete]?.isCompleted ??
468 : true)) {
469 12 : _destinationCompleters[destinationToComplete]?.complete();
470 : }
471 8 : destinationToComplete = destinationToComplete.settings.redirectedFrom;
472 : }
473 12 : final owner = _navigatorOwners[findNavigator(destination)];
474 6 : if (owner != null && (!(_destinationCompleters[owner]?.isCompleted ?? true))) {
475 6 : _destinationCompleters[owner]?.complete();
476 : }
477 : }
478 : }
|