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