Line data Source code
1 : part of '../main.dart';
2 :
3 : /// See [VRouter.mode]
4 14 : enum VRouterModes { hash, history }
5 :
6 : /// This widget handles most of the routing work
7 : /// It gives you access to the [routes] attribute where you can start
8 : /// building your routes using [VRouteElement]s
9 : ///
10 : /// Note that this widget also acts as a [MaterialApp] so you can pass
11 : /// it every argument that you would expect in [MaterialApp]
12 : class VRouter extends StatefulWidget with VRouteElement, VRouteElementSingleSubRoute {
13 : /// This list holds every possible routes of your app
14 : final List<VRouteElement> routes;
15 :
16 : /// If implemented, this becomes the default transition for every route transition
17 : /// except those who implement there own buildTransition
18 : /// Also see:
19 : /// * [VRouteElement.buildTransition] for custom local transitions
20 : ///
21 : /// Note that if this is not implemented, every route which does not implement
22 : /// its own buildTransition will be given a default transition: this of a
23 : /// [MaterialPage] or a [CupertinoPage] depending on the platform
24 : final Widget Function(
25 : Animation<double> animation, Animation<double> secondaryAnimation, Widget child)?
26 : buildTransition;
27 :
28 : /// The duration of [VRouter.buildTransition]
29 : final Duration? transitionDuration;
30 :
31 : /// The reverse duration of [VRouter.buildTransition]
32 : final Duration? reverseTransitionDuration;
33 :
34 : /// Two router mode are possible:
35 : /// - "hash": This is the default, the url will be serverAddress/#/localUrl
36 : /// - "history": This will display the url in the way we are used to, without
37 : /// the #. However note that you will need to configure your server to make this work.
38 : /// Follow the instructions here: [https://router.vuejs.org/guide/essentials/history-mode.html#example-server-configurations]
39 : final VRouterModes mode;
40 :
41 12 : @override
42 24 : Future<void> beforeEnter(VRedirector vRedirector) => _beforeEnter(vRedirector);
43 : final Future<void> Function(VRedirector vRedirector) _beforeEnter;
44 :
45 11 : @override
46 : Future<void> beforeLeave(
47 : VRedirector vRedirector,
48 : void Function(Map<String, String> historyState) saveHistoryState,
49 : ) =>
50 22 : _beforeLeave(vRedirector, saveHistoryState);
51 : final Future<void> Function(
52 : VRedirector vRedirector,
53 : void Function(Map<String, String> historyState) saveHistoryState,
54 : ) _beforeLeave;
55 :
56 12 : @override
57 : void afterEnter(BuildContext context, String? from, String to) =>
58 24 : _afterEnter(context, from, to);
59 : final void Function(BuildContext context, String? from, String to) _afterEnter;
60 :
61 7 : @override
62 14 : Future<void> onPop(VRedirector vRedirector) => _onPop(vRedirector);
63 : final Future<void> Function(VRedirector vRedirector) _onPop;
64 :
65 6 : @override
66 12 : Future<void> onSystemPop(VRedirector vRedirector) => _onSystemPop(vRedirector);
67 : final Future<void> Function(VRedirector vRedirector) _onSystemPop;
68 :
69 : /// This allows you to change the initial url
70 : ///
71 : /// The default is '/'
72 : final String initialUrl;
73 :
74 14 : VRouter({
75 : Key? key,
76 : required this.routes,
77 : Future<void> Function(VRedirector vRedirector) beforeEnter = VGuard._voidBeforeEnter,
78 : Future<void> Function(
79 : VRedirector vRedirector,
80 : void Function(Map<String, String> historyState) saveHistoryState,
81 : )
82 : beforeLeave = VGuard._voidBeforeLeave,
83 : void Function(BuildContext context, String? from, String to) afterEnter =
84 : VGuard._voidAfterEnter,
85 : Future<void> Function(VRedirector vRedirector) onPop = VPopHandler._voidOnPop,
86 : Future<void> Function(VRedirector vRedirector) onSystemPop = VPopHandler._voidOnSystemPop,
87 : this.buildTransition,
88 : this.transitionDuration,
89 : this.reverseTransitionDuration,
90 : this.mode = VRouterModes.hash,
91 : this.initialUrl = '/',
92 : // Bellow are the MaterialApp parameters
93 : this.navigatorObservers = const [],
94 : this.builder,
95 : this.title = '',
96 : this.onGenerateTitle,
97 : this.color,
98 : this.theme,
99 : this.darkTheme,
100 : this.highContrastTheme,
101 : this.highContrastDarkTheme,
102 : this.themeMode = ThemeMode.system,
103 : this.locale,
104 : this.localizationsDelegates,
105 : this.localeListResolutionCallback,
106 : this.localeResolutionCallback,
107 : this.supportedLocales = const <Locale>[Locale('en', 'US')],
108 : this.debugShowMaterialGrid = false,
109 : this.showPerformanceOverlay = false,
110 : this.checkerboardRasterCacheImages = false,
111 : this.checkerboardOffscreenLayers = false,
112 : this.showSemanticsDebugger = false,
113 : this.debugShowCheckedModeBanner = true,
114 : this.shortcuts,
115 : this.actions,
116 : }) : _beforeEnter = beforeEnter,
117 : _beforeLeave = beforeLeave,
118 : _afterEnter = afterEnter,
119 : _onPop = onPop,
120 : _onSystemPop = onSystemPop,
121 14 : super(key: key);
122 :
123 12 : @override
124 12 : VRouterState createState() => VRouterState();
125 :
126 : /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
127 : final List<NavigatorObserver> navigatorObservers;
128 :
129 : /// {@macro flutter.widgets.widgetsApp.builder}
130 : ///
131 : /// Material specific features such as [showDialog] and [showMenu], and widgets
132 : /// such as [Tooltip], [PopupMenuButton], also require a [Navigator] to properly
133 : /// function.
134 : final TransitionBuilder? builder;
135 :
136 : /// {@macro flutter.widgets.widgetsApp.title}
137 : ///
138 : /// This value is passed unmodified to [WidgetsApp.title].
139 : final String? title;
140 :
141 : /// {@macro flutter.widgets.widgetsApp.onGenerateTitle}
142 : ///
143 : /// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
144 : final GenerateAppTitle? onGenerateTitle;
145 :
146 : /// Default visual properties, like colors fonts and shapes, for this app's
147 : /// material widgets.
148 : ///
149 : /// A second [darkTheme] [ThemeData] value, which is used to provide a dark
150 : /// version of the user interface can also be specified. [themeMode] will
151 : /// control which theme will be used if a [darkTheme] is provided.
152 : ///
153 : /// The default value of this property is the value of [ThemeData.light()].
154 : ///
155 : /// See also:
156 : ///
157 : /// * [themeMode], which controls which theme to use.
158 : /// * [MediaQueryData.platformBrightness], which indicates the platform's
159 : /// desired brightness and is used to automatically toggle between [theme]
160 : /// and [darkTheme] in [MaterialApp].
161 : /// * [ThemeData.brightness], which indicates the [Brightness] of a theme's
162 : /// colors.
163 : final ThemeData? theme;
164 :
165 : /// The [ThemeData] to use when a 'dark mode' is requested by the system.
166 : ///
167 : /// Some host platforms allow the users to select a system-wide 'dark mode',
168 : /// or the application may want to offer the user the ability to choose a
169 : /// dark theme just for this application. This is theme that will be used for
170 : /// such cases. [themeMode] will control which theme will be used.
171 : ///
172 : /// This theme should have a [ThemeData.brightness] set to [Brightness.dark].
173 : ///
174 : /// Uses [theme] instead when null. Defaults to the value of
175 : /// [ThemeData.light()] when both [darkTheme] and [theme] are null.
176 : ///
177 : /// See also:
178 : ///
179 : /// * [themeMode], which controls which theme to use.
180 : /// * [MediaQueryData.platformBrightness], which indicates the platform's
181 : /// desired brightness and is used to automatically toggle between [theme]
182 : /// and [darkTheme] in [MaterialApp].
183 : /// * [ThemeData.brightness], which is typically set to the value of
184 : /// [MediaQueryData.platformBrightness].
185 : final ThemeData? darkTheme;
186 :
187 : /// The [ThemeData] to use when 'high contrast' is requested by the system.
188 : ///
189 : /// Some host platforms (for example, iOS) allow the users to increase
190 : /// contrast through an accessibility setting.
191 : ///
192 : /// Uses [theme] instead when null.
193 : ///
194 : /// See also:
195 : ///
196 : /// * [MediaQueryData.highContrast], which indicates the platform's
197 : /// desire to increase contrast.
198 : final ThemeData? highContrastTheme;
199 :
200 : /// The [ThemeData] to use when a 'dark mode' and 'high contrast' is requested
201 : /// by the system.
202 : ///
203 : /// Some host platforms (for example, iOS) allow the users to increase
204 : /// contrast through an accessibility setting.
205 : ///
206 : /// This theme should have a [ThemeData.brightness] set to [Brightness.dark].
207 : ///
208 : /// Uses [darkTheme] instead when null.
209 : ///
210 : /// See also:
211 : ///
212 : /// * [MediaQueryData.highContrast], which indicates the platform's
213 : /// desire to increase contrast.
214 : final ThemeData? highContrastDarkTheme;
215 :
216 : /// Determines which theme will be used by the application if both [theme]
217 : /// and [darkTheme] are provided.
218 : ///
219 : /// If set to [ThemeMode.system], the choice of which theme to use will
220 : /// be based on the user's system preferences. If the [MediaQuery.platformBrightnessOf]
221 : /// is [Brightness.light], [theme] will be used. If it is [Brightness.dark],
222 : /// [darkTheme] will be used (unless it is null, in which case [theme]
223 : /// will be used.
224 : ///
225 : /// If set to [ThemeMode.light] the [theme] will always be used,
226 : /// regardless of the user's system preference.
227 : ///
228 : /// If set to [ThemeMode.dark] the [darkTheme] will be used
229 : /// regardless of the user's system preference. If [darkTheme] is null
230 : /// then it will fallback to using [theme].
231 : ///
232 : /// The default value is [ThemeMode.system].
233 : ///
234 : /// See also:
235 : ///
236 : /// * [theme], which is used when a light mode is selected.
237 : /// * [darkTheme], which is used when a dark mode is selected.
238 : /// * [ThemeData.brightness], which indicates to various parts of the
239 : /// system what kind of theme is being used.
240 : final ThemeMode? themeMode;
241 :
242 : /// {@macro flutter.widgets.widgetsApp.color}
243 : final Color? color;
244 :
245 : /// {@macro flutter.widgets.widgetsApp.locale}
246 : final Locale? locale;
247 :
248 : /// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
249 : ///
250 : /// Internationalized apps that require translations for one of the locales
251 : /// listed in [GlobalMaterialLocalizations] should specify this parameter
252 : /// and list the [supportedLocales] that the application can handle.
253 : ///
254 : /// ```dart
255 : /// import 'package:flutter_localizations/flutter_localizations.dart';
256 : /// MaterialApp(
257 : /// localizationsDelegates: [
258 : /// // ... app-specific localization delegate[s] here
259 : /// GlobalMaterialLocalizations.delegate,
260 : /// GlobalWidgetsLocalizations.delegate,
261 : /// ],
262 : /// supportedLocales: [
263 : /// const Locale('en', 'US'), // English
264 : /// const Locale('he', 'IL'), // Hebrew
265 : /// // ... other locales the app supports
266 : /// ],
267 : /// // ...
268 : /// )
269 : /// ```
270 : ///
271 : /// ## Adding localizations for a new locale
272 : ///
273 : /// The information that follows applies to the unusual case of an app
274 : /// adding translations for a language not already supported by
275 : /// [GlobalMaterialLocalizations].
276 : ///
277 : /// Delegates that produce [WidgetsLocalizations] and [MaterialLocalizations]
278 : /// are included automatically. Apps can provide their own versions of these
279 : /// localizations by creating implementations of
280 : /// [LocalizationsDelegate<WidgetsLocalizations>] or
281 : /// [LocalizationsDelegate<MaterialLocalizations>] whose load methods return
282 : /// custom versions of [WidgetsLocalizations] or [MaterialLocalizations].
283 : ///
284 : /// For example: to add support to [MaterialLocalizations] for a
285 : /// locale it doesn't already support, say `const Locale('foo', 'BR')`,
286 : /// one could just extend [DefaultMaterialLocalizations]:
287 : ///
288 : /// ```dart
289 : /// class FooLocalizations extends DefaultMaterialLocalizations {
290 : /// FooLocalizations(Locale locale) : super(locale);
291 : /// @override
292 : /// String get okButtonLabel {
293 : /// if (locale == const Locale('foo', 'BR'))
294 : /// return 'foo';
295 : /// return super.okButtonLabel;
296 : /// }
297 : /// }
298 : ///
299 : /// ```
300 : ///
301 : /// A `FooLocalizationsDelegate` is essentially just a method that constructs
302 : /// a `FooLocalizations` object. We return a [SynchronousFuture] here because
303 : /// no asynchronous work takes place upon "loading" the localizations object.
304 : ///
305 : /// ```dart
306 : /// class FooLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
307 : /// const FooLocalizationsDelegate();
308 : /// @override
309 : /// Future<FooLocalizations> load(Locale locale) {
310 : /// return SynchronousFuture(FooLocalizations(locale));
311 : /// }
312 : /// @override
313 : /// bool shouldReload(FooLocalizationsDelegate old) => false;
314 : /// }
315 : /// ```
316 : ///
317 : /// Constructing a [MaterialApp] with a `FooLocalizationsDelegate` overrides
318 : /// the automatically included delegate for [MaterialLocalizations] because
319 : /// only the first delegate of each [LocalizationsDelegate.type] is used and
320 : /// the automatically included delegates are added to the end of the app's
321 : /// [localizationsDelegates] list.
322 : ///
323 : /// ```dart
324 : /// MaterialApp(
325 : /// localizationsDelegates: [
326 : /// const FooLocalizationsDelegate(),
327 : /// ],
328 : /// // ...
329 : /// )
330 : /// ```
331 : /// See also:
332 : ///
333 : /// * [supportedLocales], which must be specified along with
334 : /// [localizationsDelegates].
335 : /// * [GlobalMaterialLocalizations], a [localizationsDelegates] value
336 : /// which provides material localizations for many languages.
337 : /// * The Flutter Internationalization Tutorial,
338 : /// <https://flutter.dev/tutorials/internationalization/>.
339 : final Iterable<LocalizationsDelegate<dynamic>>? localizationsDelegates;
340 :
341 : /// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
342 : ///
343 : /// This callback is passed along to the [WidgetsApp] built by this widget.
344 : final LocaleListResolutionCallback? localeListResolutionCallback;
345 :
346 : /// {@macro flutter.widgets.LocaleResolutionCallback}
347 : ///
348 : /// This callback is passed along to the [WidgetsApp] built by this widget.
349 : final LocaleResolutionCallback? localeResolutionCallback;
350 :
351 : /// {@macro flutter.widgets.widgetsApp.supportedLocales}
352 : ///
353 : /// It is passed along unmodified to the [WidgetsApp] built by this widget.
354 : ///
355 : /// See also:
356 : ///
357 : /// * [localizationsDelegates], which must be specified for localized
358 : /// applications.
359 : /// * [GlobalMaterialLocalizations], a [localizationsDelegates] value
360 : /// which provides material localizations for many languages.
361 : /// * The Flutter Internationalization Tutorial,
362 : /// <https://flutter.dev/tutorials/internationalization/>.
363 : final Iterable<Locale>? supportedLocales;
364 :
365 : /// Turns on a performance overlay.
366 : ///
367 : /// See also:
368 : ///
369 : /// * <https://flutter.dev/debugging/#performanceoverlay>
370 : final bool? showPerformanceOverlay;
371 :
372 : /// Turns on checkerboarding of raster cache images.
373 : final bool? checkerboardRasterCacheImages;
374 :
375 : /// Turns on checkerboarding of layers rendered to offscreen bitmaps.
376 : final bool? checkerboardOffscreenLayers;
377 :
378 : /// Turns on an overlay that shows the accessibility information
379 : /// reported by the framework.
380 : final bool? showSemanticsDebugger;
381 :
382 : /// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
383 : final bool? debugShowCheckedModeBanner;
384 :
385 : /// {@macro flutter.widgets.widgetsApp.shortcuts}
386 : /// {@tool snippet}
387 : /// This example shows how to add a single shortcut for
388 : /// [LogicalKeyboardKey.select] to the default shortcuts without needing to
389 : /// add your own [Shortcuts] widget.
390 : ///
391 : /// Alternatively, you could insert a [Shortcuts] widget with just the mapping
392 : /// you want to add between the [WidgetsApp] and its child and get the same
393 : /// effect.
394 : ///
395 : /// ```dart
396 : /// Widget build(BuildContext context) {
397 : /// return WidgetsApp(
398 : /// shortcuts: <LogicalKeySet, Intent>{
399 : /// ... WidgetsApp.defaultShortcuts,
400 : /// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
401 : /// },
402 : /// color: const Color(0xFFFF0000),
403 : /// builder: (BuildContext context, Widget child) {
404 : /// return const Placeholder();
405 : /// },
406 : /// );
407 : /// }
408 : /// ```
409 : /// {@end-tool}
410 : /// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
411 : final Map<LogicalKeySet, Intent>? shortcuts;
412 :
413 : /// {@macro flutter.widgets.widgetsApp.actions}
414 : /// {@tool snippet}
415 : /// This example shows how to add a single action handling an
416 : /// [ActivateAction] to the default actions without needing to
417 : /// add your own [Actions] widget.
418 : ///
419 : /// Alternatively, you could insert a [Actions] widget with just the mapping
420 : /// you want to add between the [WidgetsApp] and its child and get the same
421 : /// effect.
422 : ///
423 : /// ```dart
424 : /// Widget build(BuildContext context) {
425 : /// return WidgetsApp(
426 : /// actions: <Type, Action<Intent>>{
427 : /// ... WidgetsApp.defaultActions,
428 : /// ActivateAction: CallbackAction(
429 : /// onInvoke: (Intent intent) {
430 : /// // Do something here...
431 : /// return null;
432 : /// },
433 : /// ),
434 : /// },
435 : /// color: const Color(0xFFFF0000),
436 : /// builder: (BuildContext context, Widget child) {
437 : /// return const Placeholder();
438 : /// },
439 : /// );
440 : /// }
441 : /// ```
442 : /// {@end-tool}
443 : /// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
444 : final Map<Type, Action<Intent>>? actions;
445 :
446 : /// Turns on a [GridPaper] overlay that paints a baseline grid
447 : /// Material apps.
448 : ///
449 : /// Only available in checked mode.
450 : ///
451 : /// See also:
452 : ///
453 : /// * <https://material.io/design/layout/spacing-methods.html>
454 : final bool? debugShowMaterialGrid;
455 :
456 9 : static VRouterData of(BuildContext context) {
457 : VRouterData? vRouterData;
458 :
459 : // First try to get a local VRouterData
460 9 : vRouterData = context.dependOnInheritedWidgetOfExactType<LocalVRouterData>();
461 : if (vRouterData != null) {
462 : return vRouterData;
463 : }
464 :
465 : // Else try to get the root VRouterData
466 0 : vRouterData = context.dependOnInheritedWidgetOfExactType<RootVRouterData>();
467 : if (vRouterData != null) {
468 : return vRouterData;
469 : }
470 :
471 : if (vRouterData == null) {
472 0 : throw FlutterError(
473 : 'VRouter.of(context) was called with a context which does not contain a VRouter.\n'
474 : 'The context used to retrieve VRouter must be that of a widget that '
475 : 'is a descendant of a VRouter widget.');
476 : }
477 : return vRouterData;
478 : }
479 :
480 14 : @override
481 14 : List<VRouteElement> buildRoutes() => routes;
482 :
483 12 : @override
484 : void afterUpdate(BuildContext context, String? from, String to) {}
485 :
486 : @override
487 12 : Future<void> beforeUpdate(VRedirector vRedirector) async {}
488 : }
489 :
490 : class VRouterState extends State<VRouter> {
491 : /// This is a context which contains the VRouter.
492 : /// It is used is VRouter.beforeLeave for example.
493 : late BuildContext _rootVRouterContext;
494 :
495 : /// Designates the number of page we navigated since
496 : /// entering the app.
497 : /// If is only used in the web to know where we are when
498 : /// the user interacts with the browser instead of the app
499 : /// (e.g back button)
500 : late int _serialCount;
501 :
502 : /// When set to true, urlToAppState will be ignored
503 : /// You must manually reset it to true otherwise it will
504 : /// be ignored forever.
505 : bool _ignoreNextBrowserCalls = false;
506 :
507 : /// When set to false, appStateToUrl will be "ignored"
508 : /// i.e. no new history entry will be created
509 : /// You must manually reset it to true otherwise it will
510 : /// be ignored forever.
511 : bool _doReportBackUrlToBrowser = true;
512 :
513 : /// Those are used in the root navigator
514 : /// They are here to prevent breaking animations
515 : final GlobalKey<NavigatorState> _navigatorKey;
516 : final HeroController _heroController;
517 :
518 : /// The child of this widget
519 : ///
520 : /// This will contain the navigator etc.
521 : //
522 : // When the app starts, before we process the '/' route, we display
523 : // nothing.
524 : // Ideally this should never be needed, or replaced with a splash screen
525 : // Should we add the option ?
526 24 : late VRoute _vRoute = VRoute(
527 12 : pages: [],
528 12 : pathParameters: {},
529 24 : vRouteElementNode: VRouteElementNode(widget, localPath: null),
530 24 : vRouteElements: [widget],
531 : );
532 :
533 : /// Every VWidgetGuard will be registered here
534 : List<VWidgetGuardMessageRoot> _vWidgetGuardMessagesRoot = [];
535 :
536 12 : VRouterState()
537 12 : : _navigatorKey = GlobalKey<NavigatorState>(),
538 12 : _heroController = HeroController();
539 :
540 : /// Url currently synced with the state
541 : /// This url can differ from the once of the browser if
542 : /// the state has been yet been updated
543 : String? url;
544 :
545 : /// Previous url that was synced with the state
546 : String? previousUrl;
547 :
548 : /// This state is saved in the browser history. This means that if the user presses
549 : /// the back or forward button on the navigator, this historyState will be the same
550 : /// as the last one you saved.
551 : ///
552 : /// It can be changed by using [context.vRouter.replaceHistoryState(newState)]
553 : Map<String, String> historyState = {};
554 :
555 : /// Maps all route parameters (i.e. parameters of the path
556 : /// mentioned as ":someId")
557 : Map<String, String> pathParameters = <String, String>{};
558 :
559 : /// Contains all query parameters (i.e. parameters after
560 : /// the "?" in the url) of the current url
561 : Map<String, String> queryParameters = <String, String>{};
562 :
563 12 : @override
564 : void initState() {
565 : // When the app starts, get the serialCount. Default to 0.
566 12 : _serialCount = (kIsWeb) ? (BrowserHelpers.getHistorySerialCount() ?? 0) : 0;
567 :
568 : // Setup the url strategy (if hash, do nothing since it is the default)
569 36 : if (widget.mode == VRouterModes.history) {
570 0 : setPathUrlStrategy();
571 : }
572 :
573 : // Check if this is the first route
574 24 : if (_serialCount == 0) {
575 : // If it is, navigate to initial url if this is not the default one
576 36 : if (widget.initialUrl != '/') {
577 : // If we are deep-linking, do not use initial url
578 0 : if (!kIsWeb || BrowserHelpers.getPathAndQuery(routerMode: widget.mode).isEmpty) {
579 15 : WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
580 15 : pushReplacement(widget.initialUrl);
581 : });
582 : }
583 : }
584 : }
585 :
586 : // If we are on the web, we listen to any unload event.
587 : // This allows us to call beforeLeave when the browser or the tab
588 : // is being closed for example
589 : if (kIsWeb) {
590 0 : BrowserHelpers.onBrowserBeforeUnload.listen((e) => _onBeforeUnload());
591 : }
592 :
593 12 : super.initState();
594 : }
595 :
596 12 : @override
597 : Widget build(BuildContext context) {
598 12 : return SimpleUrlHandler(
599 12 : urlToAppState: (BuildContext context, RouteInformation routeInformation) async {
600 24 : if (routeInformation.location != null && !_ignoreNextBrowserCalls) {
601 : // Get the new state
602 : final newState = (kIsWeb)
603 0 : ? Map<String, dynamic>.from(jsonDecode((routeInformation.state as String?) ??
604 0 : (BrowserHelpers.getHistoryState() ?? '{}')))
605 12 : : <String, dynamic>{};
606 :
607 : // Get the new serial count
608 : int? newSerialCount;
609 : try {
610 12 : newSerialCount = newState['serialCount'];
611 : // ignore: empty_catches
612 0 : } on FormatException {}
613 :
614 : // Get the new history state
615 : final newHistoryState =
616 36 : Map<String, String>.from(jsonDecode(newState['historyState'] ?? '{}'));
617 :
618 : // Check if this is the first route
619 0 : if (newSerialCount == null || newSerialCount == 0) {
620 : // If so, check is the url reported by the browser is the same as the initial url
621 : // We check "routeInformation.location == '/'" to enable deep linking
622 24 : if (routeInformation.location == '/' &&
623 48 : routeInformation.location != widget.initialUrl) {
624 : return;
625 : }
626 : }
627 :
628 : // Update the app with the new url
629 24 : await _updateUrl(
630 12 : routeInformation.location!,
631 : newHistoryState: newHistoryState,
632 : fromBrowser: true,
633 24 : newSerialCount: newSerialCount ?? _serialCount + 1,
634 : );
635 : }
636 : return null;
637 : },
638 12 : appStateToUrl: () {
639 12 : return _doReportBackUrlToBrowser
640 12 : ? RouteInformation(
641 12 : location: url ?? '/',
642 24 : state: jsonEncode({
643 12 : 'serialCount': _serialCount,
644 24 : 'historyState': jsonEncode(historyState),
645 : }),
646 : )
647 : : null;
648 : },
649 12 : child: NotificationListener<VWidgetGuardMessageRoot>(
650 1 : onNotification: (VWidgetGuardMessageRoot vWidgetGuardMessageRoot) {
651 2 : _vWidgetGuardMessagesRoot.removeWhere((message) =>
652 0 : message.vWidgetGuard.key == vWidgetGuardMessageRoot.vWidgetGuard.key);
653 2 : _vWidgetGuardMessagesRoot.add(vWidgetGuardMessageRoot);
654 :
655 : return true;
656 : },
657 12 : child: RootVRouterData(
658 : state: this,
659 12 : previousUrl: previousUrl,
660 12 : url: url,
661 12 : pathParameters: pathParameters,
662 12 : historyState: historyState,
663 12 : queryParameters: queryParameters,
664 12 : child: Builder(
665 12 : builder: (context) {
666 12 : _rootVRouterContext = context;
667 :
668 12 : final child = VRouterHelper(
669 36 : pages: _vRoute.pages.isNotEmpty
670 24 : ? _vRoute.pages
671 12 : : [
672 24 : MaterialPage(child: Container()),
673 : ],
674 12 : navigatorKey: _navigatorKey,
675 48 : observers: [_heroController, ...widget.navigatorObservers],
676 12 : backButtonDispatcher: RootBackButtonDispatcher(),
677 0 : onPopPage: (_, __) {
678 0 : _pop(
679 0 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
680 0 : pathParameters: pathParameters,
681 : );
682 : return false;
683 : },
684 0 : onSystemPopPage: () async {
685 0 : await _systemPop(
686 0 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
687 0 : pathParameters: pathParameters,
688 : );
689 : return true;
690 : },
691 : );
692 :
693 24 : return widget.builder?.call(context, child) ?? child;
694 : },
695 : ),
696 : ),
697 : ),
698 24 : title: widget.title ?? '',
699 24 : onGenerateTitle: widget.onGenerateTitle,
700 24 : color: widget.color,
701 24 : theme: widget.theme,
702 24 : darkTheme: widget.darkTheme,
703 24 : highContrastTheme: widget.highContrastTheme,
704 24 : highContrastDarkTheme: widget.highContrastDarkTheme,
705 24 : themeMode: widget.themeMode,
706 24 : locale: widget.locale,
707 24 : localizationsDelegates: widget.localizationsDelegates,
708 24 : localeListResolutionCallback: widget.localeListResolutionCallback,
709 24 : localeResolutionCallback: widget.localeResolutionCallback,
710 24 : supportedLocales: widget.supportedLocales ?? const <Locale>[Locale('en', 'US')],
711 24 : debugShowMaterialGrid: widget.debugShowMaterialGrid ?? false,
712 24 : showPerformanceOverlay: widget.showPerformanceOverlay ?? false,
713 24 : checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages ?? false,
714 24 : checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers ?? false,
715 24 : showSemanticsDebugger: widget.showSemanticsDebugger ?? false,
716 24 : debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner ?? true,
717 24 : shortcuts: widget.shortcuts,
718 24 : actions: widget.actions,
719 : );
720 : }
721 :
722 : /// Updates every state variables of [VRouter]
723 : ///
724 : /// Note that this does not call setState
725 12 : void _updateStateVariables(
726 : VRoute vRoute,
727 : String newUrl, {
728 : required Map<String, String> queryParameters,
729 : required Map<String, String> historyState,
730 : required List<VRouteElement> deactivatedVRouteElements,
731 : }) {
732 : // Update the vRoute
733 12 : this._vRoute = vRoute;
734 :
735 : // Update the urls
736 24 : previousUrl = url;
737 12 : url = newUrl;
738 :
739 : // Update the history state
740 12 : this.historyState = historyState;
741 :
742 : // Update the path parameters
743 24 : this.pathParameters = vRoute.pathParameters;
744 :
745 : // Update the query parameters
746 12 : this.queryParameters = queryParameters;
747 :
748 : // Update _vWidgetGuardMessagesRoot by removing the no-longer actives VWidgetGuards
749 25 : _vWidgetGuardMessagesRoot.removeWhere((vWidgetGuardMessageRoot) =>
750 2 : deactivatedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement));
751 : }
752 :
753 : /// See [VRouterMethodsHolder.pushNamed]
754 6 : void _updateUrlFromName(
755 : String name, {
756 : Map<String, String> pathParameters = const {},
757 : Map<String, String> queryParameters = const {},
758 : Map<String, String> newHistoryState = const {},
759 : bool isReplacement = false,
760 : }) {
761 : // Encode the path parameters
762 : pathParameters =
763 15 : pathParameters.map((key, value) => MapEntry(key, Uri.encodeComponent(value)));
764 :
765 : // We use VRouteElement.getPathFromName
766 12 : final getPathFromNameResult = widget.getPathFromName(
767 : name,
768 : pathParameters: pathParameters,
769 12 : parentPathResult: ValidParentPathResult(path: null, pathParameters: {}),
770 : remainingPathParameters: pathParameters,
771 : );
772 :
773 6 : if (getPathFromNameResult is ErrorGetPathFromNameResult) {
774 : throw getPathFromNameResult;
775 : }
776 :
777 5 : var newPath = (getPathFromNameResult as ValidNameResult).path;
778 :
779 : // Encode the path parameters
780 5 : final encodedPathParameters = pathParameters.map<String, String>(
781 6 : (key, value) => MapEntry(key, Uri.encodeComponent(value)),
782 : );
783 :
784 : // Inject the encoded path parameters into the new path
785 10 : newPath = pathToFunction(newPath)(encodedPathParameters);
786 :
787 : // Update the url with the found and completed path
788 5 : _updateUrl(newPath, queryParameters: queryParameters, isReplacement: isReplacement);
789 : }
790 :
791 : /// This should be the only way to change a url.
792 : /// Navigation cycle:
793 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
794 : /// 2. Call beforeLeave in all deactivated [VRouteElement]
795 : /// 3. Call beforeLeave in the [VRouter]
796 : /// 4. Call beforeEnter in the [VRouter]
797 : /// 5. Call beforeEnter in all initialized [VRouteElement] of the new route
798 : /// 6. Call beforeUpdate in all reused [VWidgetGuard]
799 : /// 7. Call beforeUpdate in all reused [VRouteElement]
800 : ///
801 : /// ## The history state got in beforeLeave are stored
802 : /// ## The state is updated
803 : ///
804 : /// 8. Call afterEnter in all initialized [VWidgetGuard]
805 : /// 9. Call afterEnter all initialized [VRouteElement]
806 : /// 10. Call afterEnter in the [VRouter]
807 : /// 11. Call afterUpdate in all reused [VWidgetGuard]
808 : /// 12. Call afterUpdate in all reused [VRouteElement]
809 12 : Future<void> _updateUrl(
810 : String newUrl, {
811 : Map<String, String> newHistoryState = const {},
812 : bool fromBrowser = false,
813 : int? newSerialCount,
814 : Map<String, String> queryParameters = const {},
815 : bool isUrlExternal = false,
816 : bool isReplacement = false,
817 : bool openNewTab = false,
818 : }) async {
819 0 : assert(!kIsWeb || (!fromBrowser || newSerialCount != null));
820 :
821 : // Reset this to true, new url = new chance to report
822 12 : _doReportBackUrlToBrowser = true;
823 :
824 : // This should never happen, if it does this is in error in this package
825 : // We take care of passing the right parameters depending on the platform
826 12 : assert(kIsWeb || isReplacement == false,
827 : 'This does not make sense to replace the route if you are not on the web. Please set isReplacement to false.');
828 :
829 12 : var newUri = Uri.parse(newUrl);
830 12 : final newPath = newUri.path;
831 24 : assert(!(newUri.queryParameters.isNotEmpty && queryParameters.isNotEmpty),
832 : 'You used the queryParameters attribute but the url already contained queryParameters. The latter will be overwritten by the argument you gave');
833 12 : if (queryParameters.isEmpty) {
834 12 : queryParameters = newUri.queryParameters;
835 : }
836 : // Decode queryParameters
837 12 : queryParameters = queryParameters.map(
838 3 : (key, value) => MapEntry(key, Uri.decodeComponent(value)),
839 : );
840 :
841 : // Add the queryParameters to the url if needed
842 12 : if (queryParameters.isNotEmpty) {
843 1 : newUri = Uri(path: newPath, queryParameters: queryParameters);
844 : }
845 :
846 : // Get only the path from the url
847 48 : final path = (url != null) ? Uri.parse(url!).path : null;
848 :
849 : late final List<VRouteElement> deactivatedVRouteElements;
850 : late final List<VRouteElement> reusedVRouteElements;
851 : late final List<VRouteElement> initializedVRouteElements;
852 : late final List<VWidgetGuardMessageRoot> deactivatedVWidgetGuardsMessagesRoot;
853 : late final List<VWidgetGuardMessageRoot> reusedVWidgetGuardsMessagesRoot;
854 : VRoute? newVRoute;
855 : if (isUrlExternal) {
856 : newVRoute = null;
857 0 : deactivatedVRouteElements = <VRouteElement>[];
858 0 : reusedVRouteElements = <VRouteElement>[];
859 0 : initializedVRouteElements = <VRouteElement>[];
860 0 : deactivatedVWidgetGuardsMessagesRoot = <VWidgetGuardMessageRoot>[];
861 0 : reusedVWidgetGuardsMessagesRoot = <VWidgetGuardMessageRoot>[];
862 : } else {
863 : // Get the new route
864 24 : newVRoute = widget.buildRoute(
865 12 : VPathRequestData(
866 12 : previousUrl: url,
867 : uri: newUri,
868 : historyState: newHistoryState,
869 12 : rootVRouterContext: _rootVRouterContext,
870 : ),
871 12 : parentVPathMatch: ValidVPathMatch(
872 : remainingPath: newPath,
873 12 : pathParameters: {},
874 : localPath: null,
875 : ),
876 : );
877 :
878 : if (newVRoute == null) {
879 1 : throw UnknownUrlVError(url: newUrl);
880 : }
881 :
882 : // This copy is necessary in order not to modify newVRoute.vRouteElements
883 24 : final newVRouteElements = List<VRouteElement>.from(newVRoute.vRouteElements);
884 :
885 12 : deactivatedVRouteElements = <VRouteElement>[];
886 12 : reusedVRouteElements = <VRouteElement>[];
887 36 : if (_vRoute.vRouteElements.isNotEmpty) {
888 48 : for (var vRouteElement in _vRoute.vRouteElements.reversed) {
889 : try {
890 12 : reusedVRouteElements.add(
891 12 : newVRouteElements.firstWhere(
892 24 : (newVRouteElement) => (newVRouteElement == vRouteElement),
893 : ),
894 : );
895 10 : } on StateError {
896 10 : deactivatedVRouteElements.add(vRouteElement);
897 : }
898 : }
899 : }
900 0 : initializedVRouteElements = newVRouteElements
901 12 : .where(
902 12 : (newVRouteElement) =>
903 24 : _vRoute.vRouteElements
904 48 : .indexWhere((vRouteElement) => vRouteElement == newVRouteElement) ==
905 12 : -1,
906 : )
907 12 : .toList();
908 :
909 : // Get deactivated and reused VWidgetGuards
910 12 : deactivatedVWidgetGuardsMessagesRoot = _vWidgetGuardMessagesRoot
911 13 : .where((vWidgetGuardMessageRoot) => deactivatedVRouteElements
912 2 : .contains(vWidgetGuardMessageRoot.associatedVRouteElement))
913 12 : .toList();
914 12 : reusedVWidgetGuardsMessagesRoot = _vWidgetGuardMessagesRoot
915 13 : .where((vWidgetGuardMessageRoot) =>
916 2 : reusedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
917 12 : .toList();
918 : }
919 :
920 12 : Map<String, String> historyStateToSave = {};
921 0 : void saveHistoryState(Map<String, String> historyState) {
922 0 : historyStateToSave.addAll(historyState);
923 : }
924 :
925 : // Instantiate VRedirector
926 12 : final vRedirector = VRedirector(
927 12 : context: _rootVRouterContext,
928 12 : from: url,
929 12 : to: newUri.toString(),
930 12 : previousVRouterData: RootVRouterData(
931 12 : child: Container(),
932 12 : historyState: historyState,
933 24 : pathParameters: _vRoute.pathParameters,
934 12 : queryParameters: this.queryParameters,
935 : state: this,
936 12 : url: url,
937 12 : previousUrl: previousUrl,
938 : ),
939 12 : newVRouterData: RootVRouterData(
940 12 : child: Container(),
941 : historyState: newHistoryState,
942 12 : pathParameters: newVRoute?.pathParameters ?? {},
943 : queryParameters: queryParameters,
944 : state: this,
945 12 : url: newUri.toString(),
946 12 : previousUrl: url,
947 : ),
948 : );
949 :
950 12 : if (url != null) {
951 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
952 12 : for (var vWidgetGuardMessageRoot in deactivatedVWidgetGuardsMessagesRoot) {
953 4 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeLeave(vRedirector, saveHistoryState);
954 1 : if (!vRedirector._shouldUpdate) {
955 2 : await _abortUpdateUrl(
956 : fromBrowser: fromBrowser,
957 1 : serialCount: _serialCount,
958 : newSerialCount: newSerialCount,
959 : );
960 :
961 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
962 0 : .getChildVRouteElementNode(
963 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
964 0 : _vRoute.vRouteElementNode);
965 : return;
966 : }
967 : }
968 :
969 : /// 2. Call beforeLeave in all deactivated [VRouteElement]
970 20 : for (var vRouteElement in deactivatedVRouteElements) {
971 18 : await vRouteElement.beforeLeave(vRedirector, saveHistoryState);
972 9 : if (!vRedirector._shouldUpdate) {
973 2 : await _abortUpdateUrl(
974 : fromBrowser: fromBrowser,
975 1 : serialCount: _serialCount,
976 : newSerialCount: newSerialCount,
977 : );
978 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
979 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
980 0 : _vRoute.vRouteElementNode);
981 : return;
982 : }
983 : }
984 :
985 : /// 3. Call beforeLeave in the [VRouter]
986 33 : await widget.beforeLeave(vRedirector, saveHistoryState);
987 11 : if (!vRedirector._shouldUpdate) {
988 2 : await _abortUpdateUrl(
989 : fromBrowser: fromBrowser,
990 1 : serialCount: _serialCount,
991 : newSerialCount: newSerialCount,
992 : );
993 4 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode);
994 : return;
995 : }
996 : }
997 :
998 : if (!isUrlExternal) {
999 : /// 4. Call beforeEnter in the [VRouter]
1000 36 : await widget.beforeEnter(vRedirector);
1001 12 : if (!vRedirector._shouldUpdate) {
1002 0 : await _abortUpdateUrl(
1003 : fromBrowser: fromBrowser,
1004 0 : serialCount: _serialCount,
1005 : newSerialCount: newSerialCount,
1006 : );
1007 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode);
1008 : return;
1009 : }
1010 :
1011 : /// 5. Call beforeEnter in all initialized [VRouteElement] of the new route
1012 24 : for (var vRouteElement in initializedVRouteElements) {
1013 24 : await vRouteElement.beforeEnter(vRedirector);
1014 12 : if (!vRedirector._shouldUpdate) {
1015 4 : await _abortUpdateUrl(
1016 : fromBrowser: fromBrowser,
1017 2 : serialCount: _serialCount,
1018 : newSerialCount: newSerialCount,
1019 : );
1020 5 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1021 1 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
1022 2 : _vRoute.vRouteElementNode);
1023 : return;
1024 : }
1025 : }
1026 :
1027 : /// 6. Call beforeUpdate in all reused [VWidgetGuard]
1028 13 : for (var vWidgetGuardMessageRoot in reusedVWidgetGuardsMessagesRoot) {
1029 4 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeUpdate(vRedirector);
1030 1 : if (!vRedirector._shouldUpdate) {
1031 2 : await _abortUpdateUrl(
1032 : fromBrowser: fromBrowser,
1033 1 : serialCount: _serialCount,
1034 : newSerialCount: newSerialCount,
1035 : );
1036 :
1037 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1038 0 : .getChildVRouteElementNode(
1039 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
1040 0 : _vRoute.vRouteElementNode);
1041 : return;
1042 : }
1043 : }
1044 :
1045 : /// 7. Call beforeUpdate in all reused [VRouteElement]
1046 24 : for (var vRouteElement in reusedVRouteElements) {
1047 24 : await vRouteElement.beforeUpdate(vRedirector);
1048 12 : if (!vRedirector._shouldUpdate) {
1049 2 : await _abortUpdateUrl(
1050 : fromBrowser: fromBrowser,
1051 1 : serialCount: _serialCount,
1052 : newSerialCount: newSerialCount,
1053 : );
1054 :
1055 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1056 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
1057 0 : _vRoute.vRouteElementNode);
1058 : return;
1059 : }
1060 : }
1061 : }
1062 :
1063 12 : final oldSerialCount = _serialCount;
1064 :
1065 12 : if (historyStateToSave.isNotEmpty && path != null) {
1066 : if (!kIsWeb) {
1067 0 : log(
1068 : ' WARNING: Tried to store the state $historyStateToSave while not on the web. State saving/restoration only work on the web.\n'
1069 : 'You can safely ignore this message if you just want this functionality on the web.',
1070 : name: 'VRouter',
1071 : );
1072 : } else {
1073 : /// The historyStates got in beforeLeave are stored ///
1074 : // If we come from the browser, chances are we already left the page
1075 : // So we need to:
1076 : // 1. Go back to where we were
1077 : // 2. Save the historyState
1078 : // 3. And go back again to the place
1079 0 : if (kIsWeb && fromBrowser && oldSerialCount != newSerialCount) {
1080 0 : _ignoreNextBrowserCalls = true;
1081 0 : BrowserHelpers.browserGo(oldSerialCount - newSerialCount!);
1082 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) {
1083 0 : return BrowserHelpers.getHistorySerialCount() == oldSerialCount;
1084 : });
1085 : }
1086 0 : BrowserHelpers.replaceHistoryState(jsonEncode({
1087 : 'serialCount': oldSerialCount,
1088 0 : 'historyState': jsonEncode(historyStateToSave),
1089 : }));
1090 :
1091 0 : if (kIsWeb && fromBrowser && oldSerialCount != newSerialCount) {
1092 0 : BrowserHelpers.browserGo(newSerialCount! - oldSerialCount);
1093 0 : await BrowserHelpers.onBrowserPopState.firstWhere(
1094 0 : (element) => BrowserHelpers.getHistorySerialCount() == newSerialCount);
1095 0 : _ignoreNextBrowserCalls = false;
1096 : }
1097 : }
1098 : }
1099 :
1100 : /// Leave if the url is external
1101 : if (isUrlExternal) {
1102 0 : _ignoreNextBrowserCalls = true;
1103 0 : await BrowserHelpers.pushExternal(newUri.toString(), openNewTab: openNewTab);
1104 : return;
1105 : }
1106 :
1107 : /// The state of the VRouter changes ///
1108 12 : final oldUrl = url;
1109 :
1110 : if (isReplacement) {
1111 0 : _doReportBackUrlToBrowser = false;
1112 0 : _ignoreNextBrowserCalls = true;
1113 0 : if (BrowserHelpers.getPathAndQuery(routerMode: widget.mode) != newUri.toString()) {
1114 0 : BrowserHelpers.pushReplacement(newUri.toString(), routerMode: widget.mode);
1115 0 : if (BrowserHelpers.getPathAndQuery(routerMode: widget.mode) != newUri.toString()) {
1116 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) =>
1117 0 : BrowserHelpers.getPathAndQuery(routerMode: widget.mode) == newUri.toString());
1118 : }
1119 : }
1120 0 : BrowserHelpers.replaceHistoryState(jsonEncode({
1121 0 : 'serialCount': _serialCount,
1122 0 : 'historyState': jsonEncode(newHistoryState),
1123 : }));
1124 0 : _ignoreNextBrowserCalls = false;
1125 : } else {
1126 : // If this comes from the browser, newSerialCount is not null
1127 : // If this comes from a user:
1128 : // - If he/she pushes the same url+historyState, flutter does not create a new history entry so the serialCount remains the same
1129 : // - Else the serialCount gets increased by 1
1130 12 : _serialCount = newSerialCount ??
1131 48 : _serialCount + ((newUrl != url || newHistoryState != historyState) ? 1 : 0);
1132 : }
1133 24 : setState(() {
1134 12 : _updateStateVariables(
1135 : newVRoute!,
1136 12 : newUri.toString(),
1137 : historyState: newHistoryState,
1138 : queryParameters: queryParameters,
1139 0 : deactivatedVRouteElements: deactivatedVRouteElements,
1140 : );
1141 : });
1142 :
1143 : // We need to do this after rebuild as completed so that the user can have access
1144 : // to the new state variables
1145 36 : WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
1146 : /// 8. Call afterEnter in all initialized [VWidgetGuard]
1147 : // This is done automatically by VNotificationGuard
1148 :
1149 : /// 9. Call afterEnter all initialized [VRouteElement]
1150 24 : for (var vRouteElement in initializedVRouteElements) {
1151 12 : vRouteElement.afterEnter(
1152 12 : _rootVRouterContext,
1153 : // TODO: Change this to local context? This might imply that we need a global key which is not ideal
1154 : oldUrl,
1155 12 : newUri.toString(),
1156 : );
1157 : }
1158 :
1159 : /// 10. Call afterEnter in the [VRouter]
1160 48 : widget.afterEnter(_rootVRouterContext, oldUrl, newUri.toString());
1161 :
1162 : /// 11. Call afterUpdate in all reused [VWidgetGuard]
1163 13 : for (var vWidgetGuardMessageRoot in reusedVWidgetGuardsMessagesRoot) {
1164 3 : vWidgetGuardMessageRoot.vWidgetGuard.afterUpdate(
1165 1 : vWidgetGuardMessageRoot.localContext,
1166 : oldUrl,
1167 1 : newUri.toString(),
1168 : );
1169 : }
1170 :
1171 : /// 12. Call afterUpdate in all reused [VRouteElement]
1172 24 : for (var vRouteElement in reusedVRouteElements) {
1173 12 : vRouteElement.afterUpdate(
1174 12 : _rootVRouterContext,
1175 : // TODO: Change this to local context? This might imply that we need a global key which is not ideal
1176 : oldUrl,
1177 12 : newUri.toString(),
1178 : );
1179 : }
1180 : });
1181 : }
1182 :
1183 : /// This function is used in [updateUrl] when the update should be canceled
1184 : /// This happens and vRedirector is used to stop the navigation
1185 : ///
1186 : /// On mobile nothing happens
1187 : /// On the web, if the browser already navigated away, we have to navigate back to where we were
1188 : ///
1189 : /// Note that this should be called before setState, otherwise it is useless and cannot prevent a state spread
1190 : ///
1191 : /// newSerialCount should not be null if the updateUrl came from the Browser
1192 4 : Future<void> _abortUpdateUrl({
1193 : required bool fromBrowser,
1194 : required int serialCount,
1195 : required int? newSerialCount,
1196 : }) async {
1197 : // If the url change comes from the browser, chances are the url is already changed
1198 : // So we have to navigate back to the old url (stored in _url)
1199 : // Note: in future version it would be better to delete the last url of the browser
1200 : // but it is not yet possible
1201 : if (kIsWeb &&
1202 : fromBrowser &&
1203 0 : (BrowserHelpers.getHistorySerialCount() ?? 0) != serialCount) {
1204 0 : _ignoreNextBrowserCalls = true;
1205 0 : BrowserHelpers.browserGo(serialCount - newSerialCount!);
1206 0 : await BrowserHelpers.onBrowserPopState.firstWhere((element) {
1207 0 : return BrowserHelpers.getHistorySerialCount() == serialCount;
1208 : });
1209 0 : _ignoreNextBrowserCalls = false;
1210 : }
1211 : return;
1212 : }
1213 :
1214 : /// Performs a systemPop cycle:
1215 : /// 1. Call onPop in all active [VWidgetGuards]
1216 : /// 2. Call onPop in all [VRouteElement]
1217 : /// 3. Call onPop of VRouter
1218 : /// 4. Update the url to the one found in [_defaultPop]
1219 8 : Future<void> _pop(
1220 : VRouteElement elementToPop, {
1221 : Map<String, String> pathParameters = const {},
1222 : Map<String, String> queryParameters = const {},
1223 : Map<String, String> newHistoryState = const {},
1224 : }) async {
1225 8 : assert(url != null);
1226 :
1227 : // Get information on where to pop from _defaultPop
1228 8 : final defaultPopResult = _defaultPop(
1229 : elementToPop,
1230 : pathParameters: pathParameters,
1231 : queryParameters: queryParameters,
1232 : newHistoryState: newHistoryState,
1233 : );
1234 :
1235 7 : final vRedirector = defaultPopResult.vRedirector;
1236 :
1237 7 : final poppedVRouteElements = defaultPopResult.poppedVRouteElements;
1238 :
1239 : final List<VWidgetGuardMessageRoot> poppedVWidgetGuardsMessagesRoot =
1240 7 : _vWidgetGuardMessagesRoot
1241 7 : .where((vWidgetGuardMessageRoot) =>
1242 0 : poppedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
1243 7 : .toList();
1244 :
1245 : /// 1. Call onPop in all popped [VWidgetGuards]
1246 7 : for (var vWidgetGuardMessageRoot in poppedVWidgetGuardsMessagesRoot) {
1247 0 : await vWidgetGuardMessageRoot.vWidgetGuard.onPop(vRedirector);
1248 0 : if (!vRedirector.shouldUpdate) {
1249 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1250 0 : .getChildVRouteElementNode(
1251 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
1252 0 : _vRoute.vRouteElementNode);
1253 : return;
1254 : }
1255 : }
1256 :
1257 : /// 2. Call onPop in all popped [VRouteElement]
1258 14 : for (var vRouteElement in poppedVRouteElements) {
1259 14 : await vRouteElement.onPop(vRedirector);
1260 7 : if (!vRedirector.shouldUpdate) {
1261 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1262 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
1263 0 : _vRoute.vRouteElementNode);
1264 : return;
1265 : }
1266 : }
1267 :
1268 : /// 3. Call onPop of VRouter
1269 21 : await widget.onPop(vRedirector);
1270 7 : if (!vRedirector.shouldUpdate) {
1271 0 : vRedirector._redirectFunction?.call(
1272 0 : _vRoute.vRouteElementNode.getChildVRouteElementNode(vRouteElement: widget) ??
1273 0 : _vRoute.vRouteElementNode);
1274 : return;
1275 : }
1276 :
1277 : /// 4. Update the url to the one found in [_defaultPop]
1278 7 : if (vRedirector.newVRouterData != null) {
1279 14 : _updateUrl(vRedirector.to!,
1280 : queryParameters: queryParameters, newHistoryState: newHistoryState);
1281 0 : } else if (Platform.isAndroid || Platform.isIOS) {
1282 : // If we didn't find a url to go to, we are at the start of the stack
1283 : // so we close the app on mobile
1284 0 : MoveToBackground.moveTaskToBack();
1285 : }
1286 : }
1287 :
1288 : /// Performs a systemPop cycle:
1289 : /// 1. Call onSystemPop in all active [VWidgetGuards] if implemented, else onPop
1290 : /// 2. Call onSystemPop in all [VRouteElement] if implemented, else onPop
1291 : /// 3. Call onSystemPop of VRouter if implemented, else onPop
1292 : /// 4. Update the url to the one found in [_defaultPop]
1293 6 : Future<void> _systemPop(
1294 : VRouteElement elementToPop, {
1295 : Map<String, String> pathParameters = const {},
1296 : Map<String, String> queryParameters = const {},
1297 : Map<String, String> newHistoryState = const {},
1298 : }) async {
1299 6 : assert(url != null);
1300 :
1301 : // Get information on where to pop from _defaultPop
1302 6 : final defaultPopResult = _defaultPop(
1303 : elementToPop,
1304 : pathParameters: pathParameters,
1305 : queryParameters: queryParameters,
1306 : newHistoryState: newHistoryState,
1307 : );
1308 :
1309 6 : final vRedirector = defaultPopResult.vRedirector;
1310 :
1311 6 : final poppedVRouteElements = defaultPopResult.poppedVRouteElements;
1312 :
1313 : final List<VWidgetGuardMessageRoot> poppedVWidgetGuardsMessagesRoot =
1314 6 : _vWidgetGuardMessagesRoot
1315 6 : .where((vWidgetGuardMessageRoot) =>
1316 0 : poppedVRouteElements.contains(vWidgetGuardMessageRoot.associatedVRouteElement))
1317 6 : .toList();
1318 :
1319 : /// 1. Call onSystemPop in all popping [VWidgetGuards] if implemented, else onPop
1320 6 : for (var vWidgetGuardMessageRoot in poppedVWidgetGuardsMessagesRoot) {
1321 0 : if (vWidgetGuardMessageRoot.vWidgetGuard.onSystemPop != VPopHandler._voidOnSystemPop) {
1322 0 : await vWidgetGuardMessageRoot.vWidgetGuard.onSystemPop(vRedirector);
1323 : } else {
1324 0 : await vWidgetGuardMessageRoot.vWidgetGuard.onPop(vRedirector);
1325 : }
1326 0 : if (!vRedirector.shouldUpdate) {
1327 0 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1328 0 : .getChildVRouteElementNode(
1329 0 : vRouteElement: vWidgetGuardMessageRoot.associatedVRouteElement) ??
1330 0 : _vRoute.vRouteElementNode);
1331 : return;
1332 : }
1333 : }
1334 :
1335 : /// 2. Call onSystemPop in all popped [VRouteElement] if implemented, else onPop
1336 12 : for (var vRouteElement in poppedVRouteElements) {
1337 12 : if (vRouteElement.onSystemPop != VPopHandler._voidOnSystemPop) {
1338 12 : await vRouteElement.onSystemPop(vRedirector);
1339 : } else {
1340 0 : await vRouteElement.onPop(vRedirector);
1341 : }
1342 6 : if (!vRedirector.shouldUpdate) {
1343 1 : vRedirector._redirectFunction?.call(_vRoute.vRouteElementNode
1344 0 : .getChildVRouteElementNode(vRouteElement: vRouteElement) ??
1345 0 : _vRoute.vRouteElementNode);
1346 : return;
1347 : }
1348 : }
1349 :
1350 : /// 3. Call onSystemPop of VRouter if implemented, else onPop
1351 18 : if (widget.onSystemPop != VPopHandler._voidOnSystemPop) {
1352 18 : await widget.onSystemPop(vRedirector);
1353 : } else {
1354 0 : await widget.onPop(vRedirector);
1355 : }
1356 6 : if (!vRedirector.shouldUpdate) {
1357 0 : vRedirector._redirectFunction?.call(
1358 0 : _vRoute.vRouteElementNode.getChildVRouteElementNode(vRouteElement: widget) ??
1359 0 : _vRoute.vRouteElementNode);
1360 : return;
1361 : }
1362 :
1363 : /// 4. Update the url to the one found in [_defaultPop]
1364 6 : if (vRedirector.newVRouterData != null) {
1365 12 : _updateUrl(vRedirector.to!,
1366 : queryParameters: queryParameters, newHistoryState: newHistoryState);
1367 : } else if (!kIsWeb) {
1368 : // If we didn't find a url to go to, we are at the start of the stack
1369 : // so we close the app on mobile
1370 0 : MoveToBackground.moveTaskToBack();
1371 : }
1372 : }
1373 :
1374 : /// Uses [VRouteElement.getPathFromPop] to determine the new path after popping [elementToPop]
1375 : ///
1376 : /// See:
1377 : /// * [VWidgetGuard.onPop] to override this behaviour locally
1378 : /// * [VRouteElement.onPop] to override this behaviour on a on a route level
1379 : /// * [VRouter.onPop] to override this behaviour on a global level
1380 : /// * [VWidgetGuard.onSystemPop] to override this behaviour locally
1381 : /// when the call comes from the system
1382 : /// * [VRouteElement.onSystemPop] to override this behaviour on a route level
1383 : /// when the call comes from the system
1384 : /// * [VRouter.onSystemPop] to override this behaviour on a global level
1385 : /// when the call comes from the system
1386 8 : DefaultPopResult _defaultPop(
1387 : VRouteElement elementToPop, {
1388 : Map<String, String> pathParameters = const {},
1389 : Map<String, String> queryParameters = const {},
1390 : Map<String, String> newHistoryState = const {},
1391 : }) {
1392 8 : assert(url != null);
1393 : // Encode the path parameters
1394 : pathParameters =
1395 14 : pathParameters.map((key, value) => MapEntry(key, Uri.encodeComponent(value)));
1396 :
1397 : // We don't use widget.getPathFromPop because widget.routes might have changed with a setState
1398 32 : final getPathFromPopResult = _vRoute.vRouteElementNode.vRouteElement.getPathFromPop(
1399 : elementToPop,
1400 : pathParameters: pathParameters,
1401 16 : parentPathResult: ValidParentPathResult(path: null, pathParameters: {}),
1402 : );
1403 :
1404 8 : if (getPathFromPopResult is ErrorGetPathFromPopResult) {
1405 : throw getPathFromPopResult;
1406 : }
1407 :
1408 7 : final newPath = (getPathFromPopResult as ValidPopResult).path;
1409 :
1410 : // This url will be not null if we find a route to go to
1411 : late final String? newUrl;
1412 : late final RootVRouterData? newVRouterData;
1413 :
1414 : // If newPath is empty then the app should be put in the background (for mobile)
1415 : if (newPath != null) {
1416 : // Integrate the given query parameters
1417 7 : newUrl = Uri.tryParse(newPath)
1418 14 : ?.replace(queryParameters: (queryParameters.isNotEmpty) ? queryParameters : null)
1419 7 : .toString();
1420 :
1421 7 : newVRouterData = RootVRouterData(
1422 7 : child: Container(),
1423 : historyState: newHistoryState,
1424 : pathParameters: pathParameters,
1425 : queryParameters: queryParameters,
1426 0 : url: newUrl,
1427 7 : previousUrl: url,
1428 : state: this,
1429 : );
1430 : } else {
1431 0 : newUrl = null;
1432 0 : newVRouterData = null;
1433 : }
1434 :
1435 7 : return DefaultPopResult(
1436 7 : vRedirector: VRedirector(
1437 7 : context: _rootVRouterContext,
1438 7 : from: url,
1439 0 : to: newUrl,
1440 7 : previousVRouterData: RootVRouterData(
1441 7 : child: Container(),
1442 7 : historyState: historyState,
1443 14 : pathParameters: _vRoute.pathParameters,
1444 : queryParameters: queryParameters,
1445 : state: this,
1446 7 : previousUrl: previousUrl,
1447 7 : url: url,
1448 : ),
1449 0 : newVRouterData: newVRouterData,
1450 : ),
1451 7 : poppedVRouteElements: getPathFromPopResult.poppedVRouteElements,
1452 : );
1453 : }
1454 :
1455 : /// This replaces the current history state of [VRouter] with given one
1456 0 : void replaceHistoryState(Map<String, String> newHistoryState) {
1457 0 : pushReplacement((url != null) ? Uri.parse(url!).path : '/', historyState: newHistoryState);
1458 : }
1459 :
1460 : /// WEB ONLY
1461 : /// Save the state if needed before the app gets unloaded
1462 : /// Mind that this happens when the user enter a url manually in the
1463 : /// browser so we can't prevent him from leaving the page
1464 0 : void _onBeforeUnload() async {
1465 0 : if (url == null) return;
1466 :
1467 0 : Map<String, String> historyStateToSave = {};
1468 0 : void saveHistoryState(Map<String, String> historyState) {
1469 0 : historyStateToSave.addAll(historyState);
1470 : }
1471 :
1472 : // Instantiate VRedirector
1473 0 : final vRedirector = VRedirector(
1474 0 : context: _rootVRouterContext,
1475 0 : from: url,
1476 : to: null,
1477 0 : previousVRouterData: RootVRouterData(
1478 0 : child: Container(),
1479 0 : historyState: historyState,
1480 0 : pathParameters: _vRoute.pathParameters,
1481 0 : queryParameters: this.queryParameters,
1482 : state: this,
1483 0 : url: url,
1484 0 : previousUrl: previousUrl,
1485 : ),
1486 : newVRouterData: null,
1487 : );
1488 :
1489 : /// 1. Call beforeLeave in all deactivated [VWidgetGuard]
1490 0 : for (var vWidgetGuardMessageRoot in _vWidgetGuardMessagesRoot) {
1491 0 : await vWidgetGuardMessageRoot.vWidgetGuard.beforeLeave(vRedirector, saveHistoryState);
1492 : }
1493 :
1494 : /// 2. Call beforeLeave in all deactivated [VRouteElement] and [VRouter]
1495 0 : for (var vRouteElement in _vRoute.vRouteElements.reversed) {
1496 0 : await vRouteElement.beforeLeave(vRedirector, saveHistoryState);
1497 : }
1498 :
1499 0 : if (historyStateToSave.isNotEmpty) {
1500 : /// The historyStates got in beforeLeave are stored ///
1501 0 : BrowserHelpers.replaceHistoryState(jsonEncode({
1502 0 : 'serialCount': _serialCount,
1503 0 : 'historyState': jsonEncode(historyStateToSave),
1504 : }));
1505 : }
1506 : }
1507 :
1508 : /// Starts a pop cycle
1509 : ///
1510 : /// Pop cycle:
1511 : /// 1. onPop is called in all [VNavigationGuard]s
1512 : /// 2. onPop is called in all [VRouteElement]s of the current route
1513 : /// 3. onPop is called in [VRouter]
1514 : ///
1515 : /// In any of the above steps, we can use [vRedirector] if you want to redirect or
1516 : /// stop the navigation
1517 3 : Future<void> pop({
1518 : Map<String, String> pathParameters = const {},
1519 : Map<String, String> queryParameters = const {},
1520 : Map<String, String> newHistoryState = const {},
1521 : }) async {
1522 3 : _pop(
1523 9 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
1524 : pathParameters: pathParameters,
1525 : queryParameters: queryParameters,
1526 : newHistoryState: newHistoryState,
1527 : );
1528 : }
1529 :
1530 : /// Starts a systemPop cycle
1531 : ///
1532 : /// systemPop cycle:
1533 : /// 1. onSystemPop (or onPop if not implemented) is called in all VNavigationGuards
1534 : /// 2. onSystemPop (or onPop if not implemented) is called in the nested-most VRouteElement of the current route
1535 : /// 3. onSystemPop (or onPop if not implemented) is called in VRouter
1536 : ///
1537 : /// In any of the above steps, we can use a [VRedirector] if you want to redirect or
1538 : /// stop the navigation
1539 2 : Future<void> systemPop({
1540 : Map<String, String> pathParameters = const {},
1541 : Map<String, String> queryParameters = const {},
1542 : Map<String, String> newHistoryState = const {},
1543 : }) async {
1544 2 : _systemPop(
1545 6 : _vRoute.vRouteElementNode.getVRouteElementToPop(),
1546 : pathParameters: pathParameters,
1547 : queryParameters: queryParameters,
1548 : newHistoryState: newHistoryState,
1549 : );
1550 : }
1551 :
1552 : /// Pushes the new route of the given url on top of the current one
1553 : /// A path can be of one of two forms:
1554 : /// * stating with '/', in which case we just navigate
1555 : /// to the given path
1556 : /// * not starting with '/', in which case we append the
1557 : /// current path to the given one
1558 : ///
1559 : /// We can also specify queryParameters, either by directly
1560 : /// putting them is the url or by providing a Map using [queryParameters]
1561 : ///
1562 : /// We can also put a state to the next route, this state will
1563 : /// be a router state (this is the only kind of state that we can
1564 : /// push) accessible with VRouter.of(context).historyState
1565 11 : void push(
1566 : String newUrl, {
1567 : Map<String, String> queryParameters = const {},
1568 : Map<String, String> historyState = const {},
1569 : }) {
1570 11 : if (!newUrl.startsWith('/')) {
1571 2 : if (url == null) {
1572 1 : throw InvalidPushVError(url: newUrl);
1573 : }
1574 3 : final currentPath = Uri.parse(url!).path;
1575 4 : newUrl = currentPath + (currentPath.endsWith('/') ? '' : '/') + '$newUrl';
1576 : }
1577 :
1578 11 : _updateUrl(
1579 : newUrl,
1580 : queryParameters: queryParameters,
1581 : newHistoryState: historyState,
1582 : );
1583 : }
1584 :
1585 : /// Updates the url given a [VRouteElement] name
1586 : ///
1587 : /// We can also specify path parameters to inject into the new path
1588 : ///
1589 : /// We can also specify queryParameters, either by directly
1590 : /// putting them is the url or by providing a Map using [queryParameters]
1591 : ///
1592 : /// We can also put a state to the next route, this state will
1593 : /// be a router state (this is the only kind of state that we can
1594 : /// push) accessible with VRouter.of(context).historyState
1595 : ///
1596 : /// After finding the url and taking charge of the path parameters,
1597 : /// it updates the url
1598 : ///
1599 : /// To specify a name, see [VRouteElement.name]
1600 6 : void pushNamed(
1601 : String name, {
1602 : Map<String, String> pathParameters = const {},
1603 : Map<String, String> queryParameters = const {},
1604 : Map<String, String> historyState = const {},
1605 : }) {
1606 6 : _updateUrlFromName(name,
1607 : pathParameters: pathParameters,
1608 : queryParameters: queryParameters,
1609 : newHistoryState: historyState);
1610 : }
1611 :
1612 : /// Replace the current one by the new route corresponding to the given url
1613 : /// The difference with [push] is that this overwrites the current browser history entry
1614 : /// If you are on mobile, this is the same as push
1615 : /// Path can be of one of two forms:
1616 : /// * stating with '/', in which case we just navigate
1617 : /// to the given path
1618 : /// * not starting with '/', in which case we append the
1619 : /// current path to the given one
1620 : ///
1621 : /// We can also specify queryParameters, either by directly
1622 : /// putting them is the url or by providing a Map using [queryParameters]
1623 : ///
1624 : /// We can also put a state to the next route, this state will
1625 : /// be a router state (this is the only kind of state that we can
1626 : /// push) accessible with VRouter.of(context).historyState
1627 5 : void pushReplacement(
1628 : String newUrl, {
1629 : Map<String, String> queryParameters = const {},
1630 : Map<String, String> historyState = const {},
1631 : }) {
1632 : // If not on the web, this is the same as push
1633 : if (!kIsWeb) {
1634 5 : return push(newUrl, queryParameters: queryParameters, historyState: historyState);
1635 : }
1636 :
1637 0 : if (!newUrl.startsWith('/')) {
1638 0 : if (url == null) {
1639 0 : throw InvalidPushVError(url: newUrl);
1640 : }
1641 0 : final currentPath = Uri.parse(url!).path;
1642 0 : newUrl = currentPath + '/$newUrl';
1643 : }
1644 :
1645 : // Update the url, setting isReplacement to true
1646 0 : _updateUrl(
1647 : newUrl,
1648 : queryParameters: queryParameters,
1649 : newHistoryState: historyState,
1650 : isReplacement: true,
1651 : );
1652 : }
1653 :
1654 : /// Replace the url given a [VRouteElement] name
1655 : /// The difference with [pushNamed] is that this overwrites the current browser history entry
1656 : ///
1657 : /// We can also specify path parameters to inject into the new path
1658 : ///
1659 : /// We can also specify queryParameters, either by directly
1660 : /// putting them is the url or by providing a Map using [queryParameters]
1661 : ///
1662 : /// We can also put a state to the next route, this state will
1663 : /// be a router state (this is the only kind of state that we can
1664 : /// push) accessible with VRouter.of(context).historyState
1665 : ///
1666 : /// After finding the url and taking charge of the path parameters
1667 : /// it updates the url
1668 : ///
1669 : /// To specify a name, see [VPath.name]
1670 0 : void pushReplacementNamed(
1671 : String name, {
1672 : Map<String, String> pathParameters = const {},
1673 : Map<String, String> queryParameters = const {},
1674 : Map<String, String> historyState = const {},
1675 : }) {
1676 0 : _updateUrlFromName(name,
1677 : pathParameters: pathParameters,
1678 : queryParameters: queryParameters,
1679 : newHistoryState: historyState,
1680 : isReplacement: true);
1681 : }
1682 :
1683 : /// Goes to an url which is not in the app
1684 : ///
1685 : /// On the web, you can set [openNewTab] to true to open this url
1686 : /// in a new tab
1687 0 : void pushExternal(String newUrl, {bool openNewTab = false}) =>
1688 0 : _updateUrl(newUrl, isUrlExternal: true, openNewTab: openNewTab);
1689 : }
1690 :
1691 : class DefaultPopResult {
1692 : VRedirector vRedirector;
1693 : List<VRouteElement> poppedVRouteElements;
1694 :
1695 7 : DefaultPopResult({
1696 : required this.vRedirector,
1697 : required this.poppedVRouteElements,
1698 : });
1699 : }
|