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