Line data Source code
1 : part of '../main.dart';
2 :
3 : class VRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
4 : /// This list holds every possible routes of your app
5 : final List<VRouteElement> routes;
6 :
7 : /// If implemented, this becomes the default transition for every route transition
8 : /// except those who implement there own buildTransition
9 : /// Also see:
10 : /// * [VRouteElement.buildTransition] for custom local transitions
11 : ///
12 : /// Note that if this is not implemented, every route which does not implement
13 : /// its own buildTransition will be given a default transition: this of a
14 : /// [MaterialPage] or a [CupertinoPage] depending on the platform
15 : final Widget Function(
16 : Animation<double> animation, Animation<double> secondaryAnimation, Widget child)?
17 : buildTransition;
18 :
19 : /// The duration of [VRouter.buildTransition]
20 : final Duration? transitionDuration;
21 :
22 : /// The reverse duration of [VRouter.buildTransition]
23 : final Duration? reverseTransitionDuration;
24 :
25 : /// Two router mode are possible:
26 : /// - "hash": This is the default, the url will be serverAddress/#/localUrl
27 : /// - "history": This will display the url in the way we are used to, without
28 : /// the #. However note that you will need to configure your server to make this work.
29 : /// Follow the instructions here: [https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations]
30 : final VRouterModes mode;
31 :
32 : /// This allows you to change the initial url
33 : ///
34 : /// The default is '/'
35 : final String initialUrl;
36 :
37 : /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
38 : final List<NavigatorObserver> navigatorObservers;
39 :
40 : /// This is a context which contains the VRouter.
41 : /// It is used is VRouter.beforeLeave for example.
42 : late BuildContext _rootVRouterContext;
43 :
44 : /// Designates the number of page we navigated since
45 : /// entering the app.
46 : /// If is only used in the web to know where we are when
47 : /// the user interacts with the browser instead of the app
48 : /// (e.g back button)
49 : late int _serialCount;
50 :
51 : /// When set to true, urlToAppState will be ignored
52 : /// You must manually reset it to true otherwise it will
53 : /// be ignored forever.
54 : bool _ignoreNextBrowserCalls = false;
55 :
56 : /// When set to false, appStateToUrl will be "ignored"
57 : /// i.e. no new history entry will be created
58 : /// You must manually reset it to true otherwise it will
59 : /// be ignored forever.
60 : bool _doReportBackUrlToBrowser = true;
61 :
62 : /// Build widget before the pages
63 : /// The context can be used to access VRouter.of
64 : final TransitionBuilder? builder;
65 :
66 12 : VRouterDelegate({
67 : required this.routes,
68 : this.builder,
69 : this.navigatorObservers = const [],
70 : Future<void> Function(VRedirector vRedirector) beforeEnter = VGuard._voidBeforeEnter,
71 : Future<void> Function(
72 : VRedirector vRedirector,
73 : void Function(Map<String, String> historyState) saveHistoryState,
74 : )
75 : beforeLeave = VGuard._voidBeforeLeave,
76 : void Function(BuildContext context, String? from, String to) afterEnter =
77 : VGuard._voidAfterEnter,
78 : Future<void> Function(VRedirector vRedirector) onPop = VPopHandler._voidOnPop,
79 : Future<void> Function(VRedirector vRedirector) onSystemPop = VPopHandler._voidOnSystemPop,
80 : this.buildTransition,
81 : this.transitionDuration,
82 : this.reverseTransitionDuration,
83 : this.mode = VRouterModes.hash,
84 : this.initialUrl = '/',
85 12 : }) : _navigatorKey = GlobalKey<NavigatorState>(),
86 12 : _rootVRouter = RootVRouter(
87 : routes: routes,
88 : afterEnter: afterEnter,
89 : beforeEnter: beforeEnter,
90 : beforeLeave: beforeLeave,
91 : onPop: onPop,
92 : onSystemPop: onSystemPop,
93 : ) {
94 : // When the app starts, get the serialCount. Default to 0.
95 12 : _serialCount = (kIsWeb) ? (BrowserHelpers.getHistorySerialCount() ?? 0) : 0;
96 :
97 : // Setup the url strategy (if hash, do nothing since it is the default)
98 24 : if (mode == VRouterModes.history) {
99 0 : setPathUrlStrategy();
100 : }
101 :
102 : // Check if this is the first route
103 24 : if (_serialCount == 0) {
104 : // If it is, navigate to initial url if this is not the default one
105 24 : if (initialUrl != '/') {
106 : // If we are deep-linking, do not use initial url
107 0 : if (!kIsWeb || BrowserHelpers.getPathAndQuery(routerMode: mode).isEmpty) {
108 15 : WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
109 10 : pushReplacement(initialUrl);
110 : });
111 : }
112 : }
113 : }
114 :
115 : // If we are on the web, we listen to any unload event.
116 : // This allows us to call beforeLeave when the browser or the tab
117 : // is being closed for example
118 : if (kIsWeb) {
119 0 : BrowserHelpers.onBrowserBeforeUnload.listen((e) => _onBeforeUnload());
120 : }
121 : }
122 :
123 : /// Those are used in the root navigator
124 : /// They are here to prevent breaking animations
125 : final GlobalKey<NavigatorState> _navigatorKey;
126 :
127 : /// The VRouter associated to this VRouterDelegate
128 : final RootVRouter _rootVRouter;
129 :
130 : /// The child of this widget
131 : ///
132 : /// This will contain the navigator etc.
133 : //
134 : // When the app starts, before we process the '/' route, we display
135 : // nothing.
136 : // Ideally this should never be needed, or replaced with a splash screen
137 : // Should we add the option ?
138 24 : late VRoute _vRoute = VRoute(
139 12 : pages: [],
140 12 : pathParameters: {},
141 24 : vRouteElementNode: VRouteElementNode(_rootVRouter, localPath: null),
142 24 : vRouteElements: [_rootVRouter],
143 : );
144 :
145 : /// Every VWidgetGuard will be registered here
146 : List<VWidgetGuardMessageRoot> _vWidgetGuardMessagesRoot = [];
147 :
148 : /// Url currently synced with the state
149 : /// This url can differ from the once of the browser if
150 : /// the state has been yet been updated
151 : String? url;
152 :
153 : /// Previous url that was synced with the state
154 : String? previousUrl;
155 :
156 : /// This state is saved in the browser history. This means that if the user presses
157 : /// the back or forward button on the navigator, this historyState will be the same
158 : /// as the last one you saved.
159 : ///
160 : /// It can be changed by using [context.vRouter.replaceHistoryState(newState)]
161 : Map<String, String> historyState = {};
162 :
163 : /// Maps all route parameters (i.e. parameters of the path
164 : /// mentioned as ":someId")
165 : Map<String, String> pathParameters = <String, String>{};
166 :
167 : /// Contains all query parameters (i.e. parameters after
168 : /// the "?" in the url) of the current url
169 : Map<String, String> queryParameters = <String, String>{};
170 :
171 : /// Updates every state variables of [VRouter]
172 : ///
173 : /// Note that this does not call setState
174 12 : void _updateStateVariables(
175 : VRoute vRoute,
176 : String newUrl, {
177 : required Map<String, String> queryParameters,
178 : required Map<String, String> historyState,
179 : required List<VRouteElement> deactivatedVRouteElements,
180 : }) {
181 : // Update the vRoute
182 12 : this._vRoute = vRoute;
183 :
184 : // Update the urls
185 24 : previousUrl = url;
186 12 : url = newUrl;
187 :
188 : // Update the history state
189 12 : this.historyState = historyState;
190 :
191 : // Update the path parameters
192 24 : this.pathParameters = vRoute.pathParameters;
193 :
194 : // Update the query parameters
195 12 : this.queryParameters = queryParameters;
196 :
197 : // Update _vWidgetGuardMessagesRoot by removing the no-longer actives VWidgetGuards
198 25 : _vWidgetGuardMessagesRoot.removeWhere((vWidgetGuardMessageRoot) =>
199 2 : deactivatedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement));
200 : }
201 :
202 : /// See [VRouterMethodsHolder.pushNamed]
203 6 : void _updateUrlFromName(
204 : String name, {
205 : Map<String, String> pathParameters = const {},
206 : Map<String, String> queryParameters = const {},
207 : Map<String, String> newHistoryState = const {},
208 : bool isReplacement = false,
209 : }) {
210 : // Encode the path parameters
211 : pathParameters =
212 15 : pathParameters.map((key, value) => MapEntry(key, Uri.encodeComponent(value)));
213 :
214 : // We use VRouteElement.getPathFromName
215 12 : final getPathFromNameResult = _rootVRouter.getPathFromName(
216 : name,
217 : pathParameters: pathParameters,
218 12 : parentPathResult: ValidParentPathResult(path: null, pathParameters: {}),
219 : remainingPathParameters: pathParameters,
220 : );
221 :
222 6 : if (getPathFromNameResult is ErrorGetPathFromNameResult) {
223 : throw getPathFromNameResult;
224 : }
225 :
226 5 : var newPath = (getPathFromNameResult as ValidNameResult).path;
227 :
228 : // Encode the path parameters
229 5 : final encodedPathParameters = pathParameters.map<String, String>(
230 6 : (key, value) => MapEntry(key, Uri.encodeComponent(value)),
231 : );
232 :
233 : // Inject the encoded path parameters into the new path
234 10 : newPath = pathToFunction(newPath)(encodedPathParameters);
235 :
236 : // Update the url with the found and completed path
237 5 : _updateUrl(newPath, queryParameters: queryParameters, isReplacement: isReplacement);
238 : }
239 :
240 : /// This should be the only way to change a url.
241 : /// Navigation cycle:
242 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
243 : /// 2. Call beforeLeave in all deactivated [VRouteElement]
244 : /// 3. Call beforeLeave in the [VRouter]
245 : /// 4. Call beforeEnter in the [VRouter]
246 : /// 5. Call beforeEnter in all initialized [VRouteElement] of the new route
247 : /// 6. Call beforeUpdate in all reused [VWidgetGuard]
248 : /// 7. Call beforeUpdate in all reused [VRouteElement]
249 : ///
250 : /// ## The history state got in beforeLeave are stored
251 : /// ## The state is updated
252 : ///
253 : /// 8. Call afterEnter in all initialized [VWidgetGuard]
254 : /// 9. Call afterEnter all initialized [VRouteElement]
255 : /// 10. Call afterEnter in the [VRouter]
256 : /// 11. Call afterUpdate in all reused [VWidgetGuard]
257 : /// 12. Call afterUpdate in all reused [VRouteElement]
258 12 : Future<void> _updateUrl(
259 : String newUrl, {
260 : Map<String, String> newHistoryState = const {},
261 : bool fromBrowser = false,
262 : int? newSerialCount,
263 : Map<String, String> queryParameters = const {},
264 : bool isUrlExternal = false,
265 : bool isReplacement = false,
266 : bool openNewTab = false,
267 : }) async {
268 0 : assert(!kIsWeb || (!fromBrowser || newSerialCount != null));
269 :
270 : // Reset this to true, new url = new chance to report
271 12 : _doReportBackUrlToBrowser = true;
272 :
273 : // This should never happen, if it does this is in error in this package
274 : // We take care of passing the right parameters depending on the platform
275 12 : assert(kIsWeb || isReplacement == false,
276 : 'This does not make sense to replace the route if you are not on the web. Please set isReplacement to false.');
277 :
278 12 : var newUri = Uri.parse(newUrl);
279 12 : final newPath = newUri.path;
280 24 : assert(!(newUri.queryParameters.isNotEmpty && queryParameters.isNotEmpty),
281 : 'You used the queryParameters attribute but the url already contained queryParameters. The latter will be overwritten by the argument you gave');
282 12 : if (queryParameters.isEmpty) {
283 12 : queryParameters = newUri.queryParameters;
284 : }
285 : // Decode queryParameters
286 12 : queryParameters = queryParameters.map(
287 3 : (key, value) => MapEntry(key, Uri.decodeComponent(value)),
288 : );
289 :
290 : // Add the queryParameters to the url if needed
291 12 : if (queryParameters.isNotEmpty) {
292 1 : newUri = Uri(path: newPath, queryParameters: queryParameters);
293 : }
294 :
295 : // Get only the path from the url
296 48 : final path = (url != null) ? Uri.parse(url!).path : null;
297 :
298 : late final List<VRouteElement> deactivatedVRouteElements;
299 : late final List<VRouteElement> reusedVRouteElements;
300 : late final List<VRouteElement> initializedVRouteElements;
301 : late final List<VWidgetGuardMessageRoot> deactivatedVWidgetGuardsMessagesRoot;
302 : late final List<VWidgetGuardMessageRoot> reusedVWidgetGuardsMessagesRoot;
303 : VRoute? newVRoute;
304 : if (isUrlExternal) {
305 : newVRoute = null;
306 0 : deactivatedVRouteElements = <VRouteElement>[];
307 0 : reusedVRouteElements = <VRouteElement>[];
308 0 : initializedVRouteElements = <VRouteElement>[];
309 0 : deactivatedVWidgetGuardsMessagesRoot = <VWidgetGuardMessageRoot>[];
310 0 : reusedVWidgetGuardsMessagesRoot = <VWidgetGuardMessageRoot>[];
311 : } else {
312 : // Get the new route
313 24 : newVRoute = _rootVRouter.buildRoute(
314 12 : VPathRequestData(
315 12 : previousUrl: url,
316 : uri: newUri,
317 : historyState: newHistoryState,
318 12 : rootVRouterContext: _rootVRouterContext,
319 : ),
320 12 : parentVPathMatch: ValidVPathMatch(
321 : remainingPath: newPath,
322 12 : pathParameters: {},
323 : localPath: null,
324 : ),
325 : );
326 :
327 : if (newVRoute == null) {
328 1 : throw UnknownUrlVError(url: newUrl);
329 : }
330 :
331 : // This copy is necessary in order not to modify newVRoute.vRouteElements
332 24 : final newVRouteElements = List<VRouteElement>.from(newVRoute.vRouteElements);
333 :
334 12 : deactivatedVRouteElements = <VRouteElement>[];
335 12 : reusedVRouteElements = <VRouteElement>[];
336 36 : if (_vRoute.vRouteElements.isNotEmpty) {
337 48 : for (var vRouteElement in _vRoute.vRouteElements.reversed) {
338 : try {
339 12 : reusedVRouteElements.add(
340 12 : newVRouteElements.firstWhere(
341 24 : (newVRouteElement) => (newVRouteElement == vRouteElement),
342 : ),
343 : );
344 10 : } on StateError {
345 10 : deactivatedVRouteElements.add(vRouteElement);
346 : }
347 : }
348 : }
349 0 : initializedVRouteElements = newVRouteElements
350 12 : .where(
351 12 : (newVRouteElement) =>
352 24 : _vRoute.vRouteElements
353 48 : .indexWhere((vRouteElement) => vRouteElement == newVRouteElement) ==
354 12 : -1,
355 : )
356 12 : .toList();
357 :
358 : // Get deactivated and reused VWidgetGuards
359 12 : deactivatedVWidgetGuardsMessagesRoot = _vWidgetGuardMessagesRoot
360 13 : .where((vWidgetGuardMessageRoot) => deactivatedVRouteElements
361 2 : .contains(vWidgetGuardMessageRoot.associatedVRouteElement))
362 12 : .toList();
363 12 : reusedVWidgetGuardsMessagesRoot = _vWidgetGuardMessagesRoot
364 13 : .where((vWidgetGuardMessageRoot) =>
365 2 : reusedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
366 12 : .toList();
367 : }
368 :
369 12 : Map<String, String> historyStateToSave = {};
370 0 : void saveHistoryState(Map<String, String> historyState) {
371 0 : historyStateToSave.addAll(historyState);
372 : }
373 :
374 : // Instantiate VRedirector
375 12 : final vRedirector = VRedirector(
376 12 : context: _rootVRouterContext,
377 12 : from: url,
378 12 : to: newUri.toString(),
379 12 : previousVRouterData: RootVRouterData(
380 12 : child: Container(),
381 12 : historyState: historyState,
382 24 : pathParameters: _vRoute.pathParameters,
383 12 : queryParameters: this.queryParameters,
384 : state: this,
385 12 : url: url,
386 12 : previousUrl: previousUrl,
387 : ),
388 12 : newVRouterData: RootVRouterData(
389 12 : child: Container(),
390 : historyState: newHistoryState,
391 12 : pathParameters: newVRoute?.pathParameters ?? {},
392 : queryParameters: queryParameters,
393 : state: this,
394 12 : url: newUri.toString(),
395 12 : previousUrl: url,
396 : ),
397 : );
398 :
399 12 : if (url != null) {
400 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
401 12 : for (var vWidgetGuardMessageRoot in deactivatedVWidgetGuardsMessagesRoot) {
402 4 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeLeave(vRedirector, saveHistoryState);
403 1 : if (!vRedirector._shouldUpdate) {
404 2 : await _abortUpdateUrl(
405 : fromBrowser: fromBrowser,
406 1 : serialCount: _serialCount,
407 : newSerialCount: newSerialCount,
408 : );
409 :
410 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
411 0 : .getChildVRouteElementNode(
412 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
413 0 : _vRoute.vRouteElementNode);
414 : return;
415 : }
416 : }
417 :
418 : /// 2. Call beforeLeave in all deactivated [VRouteElement]
419 20 : for (var vRouteElement in deactivatedVRouteElements) {
420 18 : await vRouteElement.beforeLeave(vRedirector, saveHistoryState);
421 9 : if (!vRedirector._shouldUpdate) {
422 2 : await _abortUpdateUrl(
423 : fromBrowser: fromBrowser,
424 1 : serialCount: _serialCount,
425 : newSerialCount: newSerialCount,
426 : );
427 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
428 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
429 0 : _vRoute.vRouteElementNode);
430 : return;
431 : }
432 : }
433 :
434 : /// 3. Call beforeLeave in the [VRouter]
435 33 : await _rootVRouter.beforeLeave(vRedirector, saveHistoryState);
436 11 : if (!vRedirector._shouldUpdate) {
437 2 : await _abortUpdateUrl(
438 : fromBrowser: fromBrowser,
439 1 : serialCount: _serialCount,
440 : newSerialCount: newSerialCount,
441 : );
442 4 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode);
443 : return;
444 : }
445 : }
446 :
447 : if (!isUrlExternal) {
448 : /// 4. Call beforeEnter in the [VRouter]
449 36 : await _rootVRouter.beforeEnter(vRedirector);
450 12 : if (!vRedirector._shouldUpdate) {
451 0 : await _abortUpdateUrl(
452 : fromBrowser: fromBrowser,
453 0 : serialCount: _serialCount,
454 : newSerialCount: newSerialCount,
455 : );
456 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode);
457 : return;
458 : }
459 :
460 : /// 5. Call beforeEnter in all initialized [VRouteElement] of the new route
461 24 : for (var vRouteElement in initializedVRouteElements) {
462 24 : await vRouteElement.beforeEnter(vRedirector);
463 12 : if (!vRedirector._shouldUpdate) {
464 4 : await _abortUpdateUrl(
465 : fromBrowser: fromBrowser,
466 2 : serialCount: _serialCount,
467 : newSerialCount: newSerialCount,
468 : );
469 5 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
470 1 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
471 2 : _vRoute.vRouteElementNode);
472 : return;
473 : }
474 : }
475 :
476 : /// 6. Call beforeUpdate in all reused [VWidgetGuard]
477 13 : for (var vWidgetGuardMessageRoot in reusedVWidgetGuardsMessagesRoot) {
478 4 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeUpdate(vRedirector);
479 1 : if (!vRedirector._shouldUpdate) {
480 2 : await _abortUpdateUrl(
481 : fromBrowser: fromBrowser,
482 1 : serialCount: _serialCount,
483 : newSerialCount: newSerialCount,
484 : );
485 :
486 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
487 0 : .getChildVRouteElementNode(
488 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
489 0 : _vRoute.vRouteElementNode);
490 : return;
491 : }
492 : }
493 :
494 : /// 7. Call beforeUpdate in all reused [VRouteElement]
495 24 : for (var vRouteElement in reusedVRouteElements) {
496 24 : await vRouteElement.beforeUpdate(vRedirector);
497 12 : if (!vRedirector._shouldUpdate) {
498 2 : await _abortUpdateUrl(
499 : fromBrowser: fromBrowser,
500 1 : serialCount: _serialCount,
501 : newSerialCount: newSerialCount,
502 : );
503 :
504 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
505 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
506 0 : _vRoute.vRouteElementNode);
507 : return;
508 : }
509 : }
510 : }
511 :
512 12 : final oldSerialCount = _serialCount;
513 :
514 12 : if (historyStateToSave.isNotEmpty && path != null) {
515 : if (!kIsWeb) {
516 0 : log(
517 : ' WARNING: Tried to store the state $historyStateToSave while not on the web. State saving/restoration only work on the web.\n'
518 : 'You can safely ignore this message if you just want this functionality on the web.',
519 : name: 'VRouter',
520 : );
521 : } else {
522 : /// The historyStates got in beforeLeave are stored ///
523 : // If we come from the browser, chances are we already left the page
524 : // So we need to:
525 : // 1. Go back to where we were
526 : // 2. Save the historyState
527 : // 3. And go back again to the place
528 0 : if (kIsWeb && fromBrowser && oldSerialCount != newSerialCount) {
529 0 : _ignoreNextBrowserCalls = true;
530 0 : BrowserHelpers.browserGo(oldSerialCount - newSerialCount!);
531 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) {
532 0 : return BrowserHelpers.getHistorySerialCount() == oldSerialCount;
533 : });
534 : }
535 0 : BrowserHelpers.replaceHistoryState(jsonEncode({
536 : 'serialCount': oldSerialCount,
537 0 : 'historyState': jsonEncode(historyStateToSave),
538 : }));
539 :
540 0 : if (kIsWeb && fromBrowser && oldSerialCount != newSerialCount) {
541 0 : BrowserHelpers.browserGo(newSerialCount! - oldSerialCount);
542 0 : await BrowserHelpers.onBrowserPopState.firstWhere(
543 0 : (element) => BrowserHelpers.getHistorySerialCount() == newSerialCount);
544 0 : _ignoreNextBrowserCalls = false;
545 : }
546 : }
547 : }
548 :
549 : /// Leave if the url is external
550 : if (isUrlExternal) {
551 0 : _ignoreNextBrowserCalls = true;
552 0 : await BrowserHelpers.pushExternal(newUri.toString(), openNewTab: openNewTab);
553 : return;
554 : }
555 :
556 : /// The state of the VRouter changes ///
557 12 : final oldUrl = url;
558 :
559 : if (isReplacement) {
560 0 : _doReportBackUrlToBrowser = false;
561 0 : _ignoreNextBrowserCalls = true;
562 0 : if (BrowserHelpers.getPathAndQuery(routerMode: mode) != newUri.toString()) {
563 0 : BrowserHelpers.pushReplacement(newUri.toString(), routerMode: mode);
564 0 : if (BrowserHelpers.getPathAndQuery(routerMode: mode) != newUri.toString()) {
565 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) =>
566 0 : BrowserHelpers.getPathAndQuery(routerMode: mode) == newUri.toString());
567 : }
568 : }
569 0 : BrowserHelpers.replaceHistoryState(jsonEncode({
570 0 : 'serialCount': _serialCount,
571 0 : 'historyState': jsonEncode(newHistoryState),
572 : }));
573 0 : _ignoreNextBrowserCalls = false;
574 : } else {
575 : // If this comes from the browser, newSerialCount is not null
576 : // If this comes from a user:
577 : // - If he/she pushes the same url+historyState, flutter does not create a new history entry so the serialCount remains the same
578 : // - Else the serialCount gets increased by 1
579 12 : _serialCount = newSerialCount ??
580 48 : _serialCount + ((newUrl != url || newHistoryState != historyState) ? 1 : 0);
581 : }
582 12 : _updateStateVariables(
583 : newVRoute!,
584 12 : newUri.toString(),
585 : historyState: newHistoryState,
586 : queryParameters: queryParameters,
587 0 : deactivatedVRouteElements: deactivatedVRouteElements,
588 : );
589 12 : notifyListeners();
590 :
591 : // We need to do this after rebuild as completed so that the user can have access
592 : // to the new state variables
593 36 : WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
594 : /// 8. Call afterEnter in all initialized [VWidgetGuard]
595 : // This is done automatically by VNotificationGuard
596 :
597 : /// 9. Call afterEnter all initialized [VRouteElement]
598 24 : for (var vRouteElement in initializedVRouteElements) {
599 12 : vRouteElement.afterEnter(
600 12 : _rootVRouterContext,
601 : // TODO: Change this to local context? This might imply that we need a global key which is not ideal
602 : oldUrl,
603 12 : newUri.toString(),
604 : );
605 : }
606 :
607 : /// 10. Call afterEnter in the [VRouter]
608 48 : _rootVRouter.afterEnter(_rootVRouterContext, oldUrl, newUri.toString());
609 :
610 : /// 11. Call afterUpdate in all reused [VWidgetGuard]
611 13 : for (var vWidgetGuardMessageRoot in reusedVWidgetGuardsMessagesRoot) {
612 3 : vWidgetGuardMessageRoot.vWidgetGuard.afterUpdate(
613 1 : vWidgetGuardMessageRoot.localContext,
614 : oldUrl,
615 1 : newUri.toString(),
616 : );
617 : }
618 :
619 : /// 12. Call afterUpdate in all reused [VRouteElement]
620 24 : for (var vRouteElement in reusedVRouteElements) {
621 12 : vRouteElement.afterUpdate(
622 12 : _rootVRouterContext,
623 : // TODO: Change this to local context? This might imply that we need a global key which is not ideal
624 : oldUrl,
625 12 : newUri.toString(),
626 : );
627 : }
628 : });
629 : }
630 :
631 : /// This function is used in [updateUrl] when the update should be canceled
632 : /// This happens and vRedirector is used to stop the navigation
633 : ///
634 : /// On mobile nothing happens
635 : /// On the web, if the browser already navigated away, we have to navigate back to where we were
636 : ///
637 : /// Note that this should be called before setState, otherwise it is useless and cannot prevent a state spread
638 : ///
639 : /// newSerialCount should not be null if the updateUrl came from the Browser
640 4 : Future<void> _abortUpdateUrl({
641 : required bool fromBrowser,
642 : required int serialCount,
643 : required int? newSerialCount,
644 : }) async {
645 : // If the url change comes from the browser, chances are the url is already changed
646 : // So we have to navigate back to the old url (stored in _url)
647 : // Note: in future version it would be better to delete the last url of the browser
648 : // but it is not yet possible
649 : if (kIsWeb &&
650 : fromBrowser &&
651 0 : (BrowserHelpers.getHistorySerialCount() ?? 0) != serialCount) {
652 0 : _ignoreNextBrowserCalls = true;
653 0 : BrowserHelpers.browserGo(serialCount - newSerialCount!);
654 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) {
655 0 : return BrowserHelpers.getHistorySerialCount() == serialCount;
656 : });
657 0 : _ignoreNextBrowserCalls = false;
658 : }
659 : return;
660 : }
661 :
662 : /// Performs a systemPop cycle:
663 : /// 1. Call onPop in all active [VWidgetGuards]
664 : /// 2. Call onPop in all [VRouteElement]
665 : /// 3. Call onPop of VRouter
666 : /// 4. Update the url to the one found in [_defaultPop]
667 8 : Future<void> _pop(
668 : VRouteElement elementToPop, {
669 : Map<String, String> pathParameters = const {},
670 : Map<String, String> queryParameters = const {},
671 : Map<String, String> newHistoryState = const {},
672 : }) async {
673 8 : assert(url != null);
674 :
675 : // Get information on where to pop from _defaultPop
676 8 : final defaultPopResult = _defaultPop(
677 : elementToPop,
678 : pathParameters: pathParameters,
679 : queryParameters: queryParameters,
680 : newHistoryState: newHistoryState,
681 : );
682 :
683 7 : final vRedirector = defaultPopResult.vRedirector;
684 :
685 : final poppedVRouteElements = defaultPopResult.poppedVRouteElements;
686 21 :
687 : final List<VWidgetGuardMessageRoot> poppedVWidgetGuardsMessagesRoot =
688 0 : _vWidgetGuardMessagesRoot
689 : .where((vWidgetGuardMessageRoot) =>
690 : poppedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
691 : .toList();
692 7 :
693 : /// 1. Call onPop in all popped [VWidgetGuards]
694 : for (var vWidgetGuardMessageRoot in poppedVWidgetGuardsMessagesRoot) {
695 : await vWidgetGuardMessageRoot.vWidgetGuard.onPop(vRedirector);
696 : if (!vRedirector.shouldUpdate) {
697 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
698 21 : .getChildVRouteElementNode(
699 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
700 : _vRoute.vRouteElementNode);
701 7 : return;
702 : }
703 : }
704 14 :
705 : /// 2. Call onPop in all popped [VRouteElement]
706 : for (var vRouteElement in poppedVRouteElements) {
707 7 : await vRouteElement.onPop(vRedirector);
708 7 : if (!vRedirector.shouldUpdate) {
709 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
710 7 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
711 : _vRoute.vRouteElementNode);
712 : return;
713 7 : }
714 0 : }
715 0 :
716 0 : /// 3. Call onPop of VRouter
717 0 : await _rootVRouter.onPop(vRedirector);
718 0 : if (!vRedirector.shouldUpdate) {
719 0 : vRedirector._redirectFunction?.call(
720 : _vRoute.vRouteElementNode.getChildVRouteElementNode(vRouteElement: _rootVRouter) ??
721 : _vRoute.vRouteElementNode);
722 : return;
723 : }
724 :
725 14 : /// 4. Update the url to the one found in [_defaultPop]
726 14 : if (vRedirector.newVRouterData != null) {
727 7 : _updateUrl(vRedirector.to!,
728 1 : queryParameters: queryParameters, newHistoryState: newHistoryState);
729 0 : } else if (Platform.isAndroid || Platform.isIOS) {
730 0 : // If we didn't find a url to go to, we are at the start of the stack
731 : // so we close the app on mobile
732 : MoveToBackground.moveTaskToBack();
733 : }
734 : }
735 :
736 21 : /// Performs a systemPop cycle:
737 7 : /// 1. Call onSystemPop in all active [VWidgetGuards] if implemented, else onPop
738 0 : /// 2. Call onSystemPop in all [VRouteElement] if implemented, else onPop
739 0 : /// 3. Call onSystemPop of VRouter if implemented, else onPop
740 0 : /// 4. Update the url to the one found in [_defaultPop]
741 : Future<void> _systemPop(
742 : VRouteElement elementToPop, {
743 : Map<String, String> pathParameters = const {},
744 : Map<String, String> queryParameters = const {},
745 7 : Map<String, String> newHistoryState = const {},
746 14 : }) async {
747 : assert(url != null);
748 0 :
749 : // Get information on where to pop from _defaultPop
750 : final defaultPopResult = _defaultPop(
751 0 : elementToPop,
752 : pathParameters: pathParameters,
753 : queryParameters: queryParameters,
754 : newHistoryState: newHistoryState,
755 : );
756 :
757 : final vRedirector = defaultPopResult.vRedirector;
758 :
759 : final poppedVRouteElements = defaultPopResult.poppedVRouteElements;
760 6 :
761 : final List<VWidgetGuardMessageRoot> poppedVWidgetGuardsMessagesRoot =
762 : _vWidgetGuardMessagesRoot
763 : .where((vWidgetGuardMessageRoot) =>
764 : poppedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
765 : .toList();
766 6 :
767 : /// 1. Call onSystemPop in all popping [VWidgetGuards] if implemented, else onPop
768 : for (var vWidgetGuardMessageRoot in poppedVWidgetGuardsMessagesRoot) {
769 6 : if (vWidgetGuardMessageRoot.vWidgetGuard.onSystemPop != VPopHandler._voidOnSystemPop) {
770 : await vWidgetGuardMessageRoot.vWidgetGuard.onSystemPop(vRedirector);
771 : } else {
772 : await vWidgetGuardMessageRoot.vWidgetGuard.onPop(vRedirector);
773 : }
774 : if (!vRedirector.shouldUpdate) {
775 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
776 6 : .getChildVRouteElementNode(
777 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
778 6 : _vRoute.vRouteElementNode);
779 : return;
780 : }
781 6 : }
782 6 :
783 0 : /// 2. Call onSystemPop in all popped [VRouteElement] if implemented, else onPop
784 6 : for (var vRouteElement in poppedVRouteElements) {
785 : if (vRouteElement.onSystemPop != VPopHandler._voidOnSystemPop) {
786 : await vRouteElement.onSystemPop(vRedirector);
787 6 : } else {
788 0 : await vRouteElement.onPop(vRedirector);
789 0 : }
790 : if (!vRedirector.shouldUpdate) {
791 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
792 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
793 0 : _vRoute.vRouteElementNode);
794 0 : return;
795 0 : }
796 0 : }
797 0 :
798 : /// 3. Call onSystemPop of VRouter if implemented, else onPop
799 : if (_rootVRouter.onSystemPop != VPopHandler._voidOnSystemPop) {
800 : await _rootVRouter.onSystemPop(vRedirector);
801 : } else {
802 : await _rootVRouter.onPop(vRedirector);
803 12 : }
804 12 : if (!vRedirector.shouldUpdate) {
805 12 : vRedirector._redirectFunction?.call(
806 : _vRoute.vRouteElementNode.getChildVRouteElementNode(vRouteElement: _rootVRouter) ??
807 0 : _vRoute.vRouteElementNode);
808 : return;
809 6 : }
810 1 :
811 0 : /// 4. Update the url to the one found in [_defaultPop]
812 0 : if (vRedirector.newVRouterData != null) {
813 : _updateUrl(vRedirector.to!,
814 : queryParameters: queryParameters, newHistoryState: newHistoryState);
815 : } else if (!kIsWeb) {
816 : // If we didn't find a url to go to, we are at the start of the stack
817 : // so we close the app on mobile
818 18 : MoveToBackground.moveTaskToBack();
819 18 : }
820 : }
821 0 :
822 : /// Uses [VRouteElement.getPathFromPop] to determine the new path after popping [elementToPop]
823 6 : ///
824 0 : /// See:
825 0 : /// * [VWidgetGuard.onPop] to override this behaviour locally
826 0 : /// * [VRouteElement.onPop] to override this behaviour on a on a route level
827 : /// * [VRouter.onPop] to override this behaviour on a global level
828 : /// * [VWidgetGuard.onSystemPop] to override this behaviour locally
829 : /// when the call comes from the system
830 : /// * [VRouteElement.onSystemPop] to override this behaviour on a route level
831 6 : /// when the call comes from the system
832 12 : /// * [VRouter.onSystemPop] to override this behaviour on a global level
833 : /// when the call comes from the system
834 : DefaultPopResult _defaultPop(
835 : VRouteElement elementToPop, {
836 : Map<String, String> pathParameters = const {},
837 0 : Map<String, String> queryParameters = const {},
838 : Map<String, String> newHistoryState = const {},
839 : }) {
840 : assert(url != null);
841 : // Encode the path parameters
842 : pathParameters =
843 : pathParameters.map((key, value) => MapEntry(key, Uri.encodeComponent(value)));
844 :
845 : // We don't use widget.getPathFromPop because widget.routes might have changed with a setState
846 : final getPathFromPopResult = _vRoute.vRouteElementNode.vRouteElement.getPathFromPop(
847 : elementToPop,
848 : pathParameters: pathParameters,
849 : parentPathResult: ValidParentPathResult(path: null, pathParameters: {}),
850 : );
851 :
852 : if (getPathFromPopResult is ErrorGetPathFromPopResult) {
853 8 : throw getPathFromPopResult;
854 : }
855 :
856 : final newPath = (getPathFromPopResult as FoundPopResult).path;
857 :
858 : // This url will be not null if we find a route to go to
859 8 : late final String? newUrl;
860 : late final RootVRouterData? newVRouterData;
861 :
862 14 : // If newPath is empty then the app should be put in the background (for mobile)
863 : if (newPath != null) {
864 : // Integrate the given query parameters
865 32 : newUrl = Uri.tryParse(newPath)
866 : ?.replace(queryParameters: (queryParameters.isNotEmpty) ? queryParameters : null)
867 : .toString();
868 16 :
869 : newVRouterData = RootVRouterData(
870 : child: Container(),
871 8 : historyState: newHistoryState,
872 : pathParameters: pathParameters,
873 : queryParameters: queryParameters,
874 : url: newUrl,
875 7 : previousUrl: url,
876 : state: this,
877 : );
878 : } else {
879 : newUrl = null;
880 : newVRouterData = null;
881 : }
882 :
883 : final vNodeToPop =
884 7 : _vRoute.vRouteElementNode.getVRouteElementNodeFromVRouteElement(elementToPop);
885 14 :
886 7 : assert(vNodeToPop != null);
887 :
888 7 : // This is the list of [VRouteElement]s that where not necessary expected to pop but did because of
889 7 : // the pop of [elementToPop]
890 : final poppedVRouteElementsFromPopResult = getPathFromPopResult.poppedVRouteElements;
891 :
892 : // This is predictable list of [VRouteElement]s that are expected to pop because they are
893 0 : // in the nestedRoutes or stackedRoutes of [elementToPop] [VRouteElementNode]
894 7 : // We take the reversed because we when to call onPop in the deepest nested
895 : // [VRouteElement] first
896 : final poppedVRouteElementsFromVNode = vNodeToPop!.getVRouteElements().reversed.toList();
897 :
898 0 : // This is the list of every [VRouteElement] which should pop
899 0 : final poppedVRouteElements =
900 : poppedVRouteElementsFromVNode + poppedVRouteElementsFromPopResult;
901 :
902 7 : // elementToPop should have a duplicate so we remove it
903 7 : poppedVRouteElements.removeAt(poppedVRouteElements.indexOf(elementToPop));
904 7 :
905 7 : return DefaultPopResult(
906 0 : vRedirector: VRedirector(
907 7 : context: _rootVRouterContext,
908 7 : from: url,
909 7 : to: newUrl,
910 14 : previousVRouterData: RootVRouterData(
911 : child: Container(),
912 : historyState: historyState,
913 7 : pathParameters: _vRoute.pathParameters,
914 7 : queryParameters: queryParameters,
915 : state: this,
916 0 : previousUrl: previousUrl,
917 : url: url,
918 7 : ),
919 : newVRouterData: newVRouterData,
920 : ),
921 : poppedVRouteElements: poppedVRouteElements,
922 : );
923 0 : }
924 0 :
925 : /// This replaces the current history state of [VRouter] with given one
926 : void replaceHistoryState(Map<String, String> newHistoryState) {
927 : pushReplacement((url != null) ? Uri.parse(url!).path : '/', historyState: newHistoryState);
928 : }
929 :
930 : /// WEB ONLY
931 0 : /// Save the state if needed before the app gets unloaded
932 0 : /// Mind that this happens when the user enter a url manually in the
933 : /// browser so we can't prevent him from leaving the page
934 0 : void _onBeforeUnload() async {
935 0 : if (url == null) return;
936 0 :
937 : Map<String, String> historyStateToSave = {};
938 : void saveHistoryState(Map<String, String> historyState) {
939 : historyStateToSave.addAll(historyState);
940 0 : }
941 0 :
942 0 : // Instantiate VRedirector
943 : final vRedirector = VRedirector(
944 0 : context: _rootVRouterContext,
945 0 : from: url,
946 0 : to: null,
947 0 : previousVRouterData: RootVRouterData(
948 0 : child: Container(),
949 : historyState: historyState,
950 0 : pathParameters: _vRoute.pathParameters,
951 0 : queryParameters: this.queryParameters,
952 : state: this,
953 : url: url,
954 : previousUrl: previousUrl,
955 : ),
956 : newVRouterData: null,
957 0 : );
958 0 :
959 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
960 : for (var vWidgetGuardMessageRoot in _vWidgetGuardMessagesRoot) {
961 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeLeave(vRedirector, saveHistoryState);
962 0 : }
963 0 :
964 : /// 2. Call beforeLeave in all deactivated [VRouteElement] and [VRouter]
965 : for (var vRouteElement in _vRoute.vRouteElements.reversed) {
966 0 : await vRouteElement.beforeLeave(vRedirector, saveHistoryState);
967 : }
968 0 :
969 0 : if (historyStateToSave.isNotEmpty) {
970 0 : /// The historyStates got in beforeLeave are stored ///
971 : BrowserHelpers.replaceHistoryState(jsonEncode({
972 : 'serialCount': _serialCount,
973 : 'historyState': jsonEncode(historyStateToSave),
974 : }));
975 : }
976 : }
977 :
978 : /// Starts a pop cycle
979 : ///
980 : /// Pop cycle:
981 : /// 1. onPop is called in all [VNavigationGuard]s
982 : /// 2. onPop is called in all [VRouteElement]s of the current route
983 : /// 3. onPop is called in [VRouter]
984 3 : ///
985 : /// In any of the above steps, we can use [vRedirector] if you want to redirect or
986 : /// stop the navigation
987 : Future<void> pop({
988 : Map<String, String> pathParameters = const {},
989 3 : Map<String, String> queryParameters = const {},
990 9 : Map<String, String> newHistoryState = const {},
991 : }) async {
992 : _pop(
993 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
994 : pathParameters: pathParameters,
995 : queryParameters: queryParameters,
996 : newHistoryState: newHistoryState,
997 : );
998 : }
999 :
1000 : /// Starts a systemPop cycle
1001 : ///
1002 : /// systemPop cycle:
1003 : /// 1. onSystemPop (or onPop if not implemented) is called in all VNavigationGuards
1004 : /// 2. onSystemPop (or onPop if not implemented) is called in the nested-most VRouteElement of the current route
1005 : /// 3. onSystemPop (or onPop if not implemented) is called in VRouter
1006 2 : ///
1007 : /// In any of the above steps, we can use a [VRedirector] if you want to redirect or
1008 : /// stop the navigation
1009 : Future<void> systemPop({
1010 : Map<String, String> pathParameters = const {},
1011 2 : Map<String, String> queryParameters = const {},
1012 6 : Map<String, String> newHistoryState = const {},
1013 : }) async {
1014 : _systemPop(
1015 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
1016 : pathParameters: pathParameters,
1017 : queryParameters: queryParameters,
1018 : newHistoryState: newHistoryState,
1019 : );
1020 : }
1021 :
1022 : /// Pushes the new route of the given url on top of the current one
1023 : /// A path can be of one of two forms:
1024 : /// * stating with '/', in which case we just navigate
1025 : /// to the given path
1026 : /// * not starting with '/', in which case we append the
1027 : /// current path to the given one
1028 : ///
1029 : /// We can also specify queryParameters, either by directly
1030 : /// putting them is the url or by providing a Map using [queryParameters]
1031 : ///
1032 11 : /// We can also put a state to the next route, this state will
1033 : /// be a router state (this is the only kind of state that we can
1034 : /// push) accessible with VRouter.of(context).historyState
1035 : void push(
1036 : String newUrl, {
1037 11 : Map<String, String> queryParameters = const {},
1038 2 : Map<String, String> historyState = const {},
1039 1 : }) {
1040 : if (!newUrl.startsWith('/')) {
1041 3 : if (url == null) {
1042 4 : throw InvalidPushVError(url: newUrl);
1043 : }
1044 : final currentPath = Uri.parse(url!).path;
1045 11 : newUrl = currentPath + (currentPath.endsWith('/') ? '' : '/') + '$newUrl';
1046 : }
1047 :
1048 : _updateUrl(
1049 : newUrl,
1050 : queryParameters: queryParameters,
1051 : newHistoryState: historyState,
1052 : );
1053 : }
1054 :
1055 : /// Updates the url given a [VRouteElement] name
1056 : ///
1057 : /// We can also specify path parameters to inject into the new path
1058 : ///
1059 : /// We can also specify queryParameters, either by directly
1060 : /// putting them is the url or by providing a Map using [queryParameters]
1061 : ///
1062 : /// We can also put a state to the next route, this state will
1063 : /// be a router state (this is the only kind of state that we can
1064 : /// push) accessible with VRouter.of(context).historyState
1065 : ///
1066 : /// After finding the url and taking charge of the path parameters,
1067 6 : /// it updates the url
1068 : ///
1069 : /// To specify a name, see [VRouteElement.name]
1070 : void pushNamed(
1071 : String name, {
1072 : Map<String, String> pathParameters = const {},
1073 6 : Map<String, String> queryParameters = const {},
1074 : Map<String, String> historyState = const {},
1075 : }) {
1076 : _updateUrlFromName(name,
1077 : pathParameters: pathParameters,
1078 : queryParameters: queryParameters,
1079 : newHistoryState: historyState);
1080 : }
1081 :
1082 : /// Replace the current one by the new route corresponding to the given url
1083 : /// The difference with [push] is that this overwrites the current browser history entry
1084 : /// If you are on mobile, this is the same as push
1085 : /// Path can be of one of two forms:
1086 : /// * stating with '/', in which case we just navigate
1087 : /// to the given path
1088 : /// * not starting with '/', in which case we append the
1089 : /// current path to the given one
1090 : ///
1091 : /// We can also specify queryParameters, either by directly
1092 : /// putting them is the url or by providing a Map using [queryParameters]
1093 : ///
1094 5 : /// We can also put a state to the next route, this state will
1095 : /// be a router state (this is the only kind of state that we can
1096 : /// push) accessible with VRouter.of(context).historyState
1097 : void pushReplacement(
1098 : String newUrl, {
1099 : Map<String, String> queryParameters = const {},
1100 : Map<String, String> historyState = const {},
1101 5 : }) {
1102 : // If not on the web, this is the same as push
1103 : if (!kIsWeb) {
1104 0 : return push(newUrl, queryParameters: queryParameters, historyState: historyState);
1105 0 : }
1106 0 :
1107 : if (!newUrl.startsWith('/')) {
1108 0 : if (url == null) {
1109 0 : throw InvalidPushVError(url: newUrl);
1110 : }
1111 : final currentPath = Uri.parse(url!).path;
1112 : newUrl = currentPath + '/$newUrl';
1113 0 : }
1114 :
1115 : // Update the url, setting isReplacement to true
1116 : _updateUrl(
1117 : newUrl,
1118 : queryParameters: queryParameters,
1119 : newHistoryState: historyState,
1120 : isReplacement: true,
1121 : );
1122 : }
1123 :
1124 : /// Replace the url given a [VRouteElement] name
1125 : /// The difference with [pushNamed] is that this overwrites the current browser history entry
1126 : ///
1127 : /// We can also specify path parameters to inject into the new path
1128 : ///
1129 : /// We can also specify queryParameters, either by directly
1130 : /// putting them is the url or by providing a Map using [queryParameters]
1131 : ///
1132 : /// We can also put a state to the next route, this state will
1133 : /// be a router state (this is the only kind of state that we can
1134 : /// push) accessible with VRouter.of(context).historyState
1135 : ///
1136 : /// After finding the url and taking charge of the path parameters
1137 0 : /// it updates the url
1138 : ///
1139 : /// To specify a name, see [VPath.name]
1140 : void pushReplacementNamed(
1141 : String name, {
1142 : Map<String, String> pathParameters = const {},
1143 0 : Map<String, String> queryParameters = const {},
1144 : Map<String, String> historyState = const {},
1145 : }) {
1146 : _updateUrlFromName(name,
1147 : pathParameters: pathParameters,
1148 : queryParameters: queryParameters,
1149 : newHistoryState: historyState,
1150 : isReplacement: true);
1151 : }
1152 :
1153 : /// Goes to an url which is not in the app
1154 0 : ///
1155 0 : /// On the web, you can set [openNewTab] to true to open this url
1156 : /// in a new tab
1157 : void pushExternal(String newUrl, {bool openNewTab = false}) =>
1158 : _updateUrl(newUrl, isUrlExternal: true, openNewTab: openNewTab);
1159 0 :
1160 0 : /// handles systemPop
1161 0 : @override
1162 0 : Future<bool> popRoute() async {
1163 : await _systemPop(
1164 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
1165 : pathParameters: pathParameters,
1166 : );
1167 : return true;
1168 : }
1169 12 :
1170 24 : /// Navigation state to app state
1171 : @override
1172 : Future<void> setNewRoutePath(RouteInformation routeInformation) async {
1173 0 : if (routeInformation.location != null && !_ignoreNextBrowserCalls) {
1174 0 : // Get the new state
1175 12 : final newState = (kIsWeb)
1176 : ? Map<String, dynamic>.from(jsonDecode((routeInformation.state as String?) ??
1177 : (BrowserHelpers.getHistoryState() ?? '{}')))
1178 : : <String, dynamic>{};
1179 :
1180 12 : // Get the new serial count
1181 : int? newSerialCount;
1182 0 : try {
1183 : newSerialCount = newState['serialCount'];
1184 : // ignore: empty_catches
1185 : } on FormatException {}
1186 36 :
1187 : // Get the new history state
1188 : final newHistoryState =
1189 0 : Map<String, String>.from(jsonDecode(newState['historyState'] ?? '{}'));
1190 :
1191 : // Check if this is the first route
1192 60 : if (newSerialCount == null || newSerialCount == 0) {
1193 : // If so, check is the url reported by the browser is the same as the initial url
1194 : // We check "routeInformation.location == '/'" to enable deep linking
1195 : if (routeInformation.location == '/' && routeInformation.location != initialUrl) {
1196 : return;
1197 : }
1198 24 : }
1199 12 :
1200 : // Update the app with the new url
1201 : await _updateUrl(
1202 24 : routeInformation.location!,
1203 : newHistoryState: newHistoryState,
1204 : fromBrowser: true,
1205 : newSerialCount: newSerialCount ?? _serialCount + 1,
1206 : );
1207 : }
1208 12 : }
1209 12 :
1210 12 : /// App state to navigation state
1211 12 : @override
1212 24 : RouteInformation? get currentConfiguration => _doReportBackUrlToBrowser
1213 12 : ? RouteInformation(
1214 24 : location: url ?? '/',
1215 : state: jsonEncode({
1216 : 'serialCount': _serialCount,
1217 : 'historyState': jsonEncode(historyState),
1218 : }),
1219 12 : )
1220 : : null;
1221 12 :
1222 1 : @override
1223 2 : Widget build(BuildContext context) {
1224 0 : return NotificationListener<VWidgetGuardMessageRoot>(
1225 2 : onNotification: (VWidgetGuardMessageRoot vWidgetGuardMessageRoot) {
1226 : _vWidgetGuardMessagesRoot.removeWhere(
1227 : (message) => message.vWidgetGuard.key == vWidgetGuardMessageRoot.vWidgetGuard.key);
1228 : _vWidgetGuardMessagesRoot.add(vWidgetGuardMessageRoot);
1229 12 :
1230 : return true;
1231 12 : },
1232 12 : child: RootVRouterData(
1233 12 : state: this,
1234 12 : previousUrl: previousUrl,
1235 12 : url: url,
1236 12 : pathParameters: pathParameters,
1237 12 : historyState: historyState,
1238 12 : queryParameters: queryParameters,
1239 : child: Builder(
1240 12 : builder: (context) {
1241 36 : _rootVRouterContext = context;
1242 24 :
1243 12 : final child = Navigator(
1244 24 : pages: _vRoute.pages.isNotEmpty
1245 : ? _vRoute.pages
1246 12 : : [
1247 12 : MaterialPage(child: Container()),
1248 0 : ],
1249 0 : key: _navigatorKey,
1250 0 : observers: navigatorObservers,
1251 0 : onPopPage: (_, __) {
1252 : _pop(
1253 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
1254 : pathParameters: pathParameters,
1255 : );
1256 : return false;
1257 12 : },
1258 : );
1259 :
1260 : return builder?.call(context, child) ?? child;
1261 : },
1262 : ),
1263 : ),
1264 : );
1265 : }
1266 : }
1267 :
1268 : class DefaultPopResult {
1269 7 : VRedirector vRedirector;
1270 : List<VRouteElement> poppedVRouteElements;
1271 :
1272 : DefaultPopResult({
1273 : required this.vRedirector,
1274 : required this.poppedVRouteElements,
1275 : });
1276 : }
|