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