Line data Source code
1 : import 'dart:convert';
2 :
3 : import 'package:beamer/beamer.dart';
4 : import 'package:beamer/src/transition_delegates.dart';
5 : import 'package:flutter/foundation.dart';
6 : import 'package:flutter/material.dart';
7 : import 'package:flutter/services.dart';
8 : import 'package:flutter/widgets.dart';
9 :
10 : import 'utils.dart';
11 :
12 : /// A delegate that is used by the [Router] to build the [Navigator].
13 : ///
14 : /// This is "the beamer", the one that does the actual beaming.
15 : class BeamerDelegate extends RouterDelegate<RouteInformation>
16 : with ChangeNotifier, PopNavigatorRouterDelegateMixin<RouteInformation> {
17 6 : BeamerDelegate({
18 : required this.locationBuilder,
19 : this.initialPath = '/',
20 : this.listener,
21 : this.preferUpdate = true,
22 : this.removeDuplicateHistory = true,
23 : this.notFoundPage,
24 : this.notFoundRedirect,
25 : this.notFoundRedirectNamed,
26 : this.guards = const <BeamGuard>[],
27 : this.navigatorObservers = const <NavigatorObserver>[],
28 : this.transitionDelegate = const DefaultTransitionDelegate(),
29 : this.beamBackTransitionDelegate = const ReverseTransitionDelegate(),
30 : this.onPopPage,
31 : this.setBrowserTabTitle = true,
32 : this.updateFromParent = true,
33 : this.updateParent = true,
34 : }) {
35 6 : notFoundPage ??= const BeamPage(
36 : title: 'Not found',
37 : child: Scaffold(body: Center(child: Text('Not found'))),
38 : );
39 :
40 12 : _currentTransitionDelegate = transitionDelegate;
41 :
42 18 : configuration = RouteInformation(location: initialPath);
43 12 : _currentBeamLocation = EmptyBeamLocation();
44 : }
45 :
46 : /// A state of this delegate. This is the `routeInformation` that goes into
47 : /// [locationBuilder] to build an appropriate [BeamLocation].
48 : ///
49 : /// A way to modify this state is via [update].
50 : late RouteInformation configuration;
51 :
52 : BeamerDelegate? _parent;
53 :
54 : /// A delegate of a parent of the [Beamer] that has this delegate.
55 : ///
56 : /// This is not null only if multiple [Beamer]s are used;
57 : /// `*App.router` and at least one more [Beamer] in the Widget tree.
58 6 : BeamerDelegate? get parent => _parent;
59 3 : set parent(BeamerDelegate? parent) {
60 3 : _parent = parent!;
61 3 : _initializeFromParent();
62 3 : if (updateFromParent) {
63 9 : _parent!.addListener(_updateFromParent);
64 : }
65 : }
66 :
67 : /// The top-most [BeamerDelegate], a parent of all.
68 : ///
69 : /// It will return root even when called on root.
70 1 : BeamerDelegate get root {
71 1 : if (_parent == null) {
72 : return this;
73 : }
74 1 : var root = _parent!;
75 1 : while (root._parent != null) {
76 1 : root = root._parent!;
77 : }
78 : return root;
79 : }
80 :
81 : /// A builder for [BeamLocation]s.
82 : ///
83 : /// There are 3 ways of building an appropriate [BeamLocation] which will in
84 : /// turn build a stack of pages that should go into [Navigator.pages].
85 : ///
86 : /// 1. Custom closure
87 : /// ```dart
88 : /// locationBuilder: (state) {
89 : /// if (state.uri.pathSegments.contains('l1')) {
90 : /// return Location1(state);
91 : /// }
92 : /// if (state.uri.pathSegments.contains('l2')) {
93 : /// return Location2(state);
94 : /// }
95 : /// return NotFound(path: state.uri.toString());
96 : /// },
97 : /// ```
98 : ///
99 : /// 2. [BeamerLocationBuilder]; chooses appropriate [BeamLocation] itself
100 : /// ```dart
101 : /// locationBuilder: BeamerLocationBuilder(
102 : /// beamLocations: [
103 : /// Location1(),
104 : /// Location2(),
105 : /// ],
106 : /// ),
107 : /// ```
108 : ///
109 : /// 3. [SimpleLocationBuilder]; a Map of routes
110 : /// ```dart
111 : /// locationBuilder: SimpleLocationBuilder(
112 : /// routes: {
113 : /// '/': (context) => HomeScreen(),
114 : /// '/another': (context) => AnotherScreen(),
115 : /// },
116 : /// ),
117 : /// ```
118 : final LocationBuilder locationBuilder;
119 :
120 : /// The path to replace `/` as default initial route path upon load.
121 : ///
122 : /// Note that (if set to anything other than `/` (default)),
123 : /// you will not be able to navigate to `/` by manually typing
124 : /// it in the URL bar, because it will always be transformed to `initialPath`,
125 : /// but you will be able to get to `/` by popping pages with back button,
126 : /// if there are pages in [BeamLocation.buildPages] that will build
127 : /// when there are no path segments.
128 : final String initialPath;
129 :
130 : /// The listener for this, that will be called on every navigation event
131 : /// and will recieve the [state] and [currentBeamLocation].
132 : final void Function(RouteInformation, BeamLocation)? listener;
133 :
134 : /// Whether to prefer updating [currentBeamLocation] if it's of the same type
135 : /// as the [BeamLocation] being beamed to,
136 : /// instead of adding it to [beamLocationHistory].
137 : ///
138 : /// See how this is used at [_pushHistory] implementation.
139 : final bool preferUpdate;
140 :
141 : /// Whether to remove [BeamLocation]s from [beamLocationHistory]
142 : /// if they are the same type as the location being beamed to.
143 : ///
144 : /// See how this is used at [_pushHistory] implementation.
145 : final bool removeDuplicateHistory;
146 :
147 : /// Page to show when no [BeamLocation] supports the incoming URI.
148 : BeamPage? notFoundPage;
149 :
150 : /// [BeamLocation] to redirect to when no [BeamLocation] supports the incoming URI.
151 : final BeamLocation? notFoundRedirect;
152 :
153 : /// URI string to redirect to when no [BeamLocation] supports the incoming URI.
154 : final String? notFoundRedirectNamed;
155 :
156 : /// Guards that will be executing [check] on [currentBeamLocation] candidate.
157 : ///
158 : /// Checks will be executed in order; chain of responsibility pattern.
159 : /// When some guard returns `false`, location candidate will not be accepted
160 : /// and stack of pages will be updated as is configured in [BeamGuard].
161 : final List<BeamGuard> guards;
162 :
163 : /// The list of observers for the [Navigator].
164 : final List<NavigatorObserver> navigatorObservers;
165 :
166 : /// A transition delegate to be used by [Navigator].
167 : ///
168 : /// This transition delegate will be overridden by the one in [BeamLocation],
169 : /// if any is set.
170 : ///
171 : /// See [Navigator.transitionDelegate].
172 : final TransitionDelegate transitionDelegate;
173 :
174 : /// A transition delegate to be used by [Navigator] when beaming back.
175 : ///
176 : /// When calling [beamBack], it's useful to animate routes in reverse order;
177 : /// adding the new ones behind and then popping the current ones,
178 : /// therefore, the default is [ReverseTransitionDelegate].
179 : final TransitionDelegate beamBackTransitionDelegate;
180 :
181 : /// Callback when `pop` is requested.
182 : ///
183 : /// Return `true` if pop will be handled entirely by this function.
184 : /// Return `false` if beamer should finish handling the pop.
185 : ///
186 : /// See [build] for details on how beamer handles [Navigator.onPopPage].
187 : bool Function(BuildContext context, Route<dynamic> route, dynamic result)?
188 : onPopPage;
189 :
190 : /// Whether the title attribute of [BeamPage] should
191 : /// be used to set and update the browser tab title.
192 : final bool setBrowserTabTitle;
193 :
194 : /// Whether to call [update] when parent notifies listeners.
195 : ///
196 : /// This means that navigation can be done either on parent or on this
197 : final bool updateFromParent;
198 :
199 : /// Whether to call [update] on [parent] when [state] is updated.
200 : ///
201 : /// This means that parent's [beamStateHistory] will be in sync.
202 : final bool updateParent;
203 :
204 : final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
205 :
206 : /// {@template routeHistory}
207 : /// The history of beaming states.
208 : ///
209 : /// [BeamState] is inserted on every beaming event, if it differs from last.
210 : ///
211 : /// See [_pushHistory].
212 : /// {@endtemplate}
213 : final List<RouteInformation> routeHistory = [];
214 :
215 : /// {@template beamLocationHistory}
216 : /// The history of [BeamLocation]s.
217 : ///
218 : /// [BeamLocation] is inserted differently depending on configuration of
219 : /// [preferUpdate], [replaceCurrent], [removeDuplicateHistory].
220 : ///
221 : /// See [_pushHistory].
222 : /// {@endtemplate}
223 : final List<BeamLocation> beamLocationHistory = [];
224 :
225 : late BeamLocation _currentBeamLocation;
226 :
227 : /// {@template currentBeamLocation}
228 : /// A [BeamLocation] that is currently responsible for providing a page stack
229 : /// via [BeamLocation.buildPages] and holds the current [BeamState].
230 : ///
231 : /// Usually obtained via
232 : /// ```dart
233 : /// Beamer.of(context).currentBeamLocation
234 : /// ```
235 : /// {@endtemplate}
236 12 : BeamLocation get currentBeamLocation => _currentBeamLocation;
237 :
238 : List<BeamPage> _currentPages = [];
239 :
240 : /// {@template currentPages}
241 : /// [currentBeamLocation]'s "effective" pages, the ones that were built.
242 : /// {@endtemplate}
243 8 : List<BeamPage> get currentPages => _currentPages;
244 :
245 : /// Whether to implicitly [beamBack] instead of default pop.
246 : bool _beamBackOnPop = false;
247 :
248 : /// Whether to implicitly [popBeamLocation] instead of default pop.
249 : bool _popBeamLocationOnPop = false;
250 :
251 : /// Which transition delegate to use in the next build.
252 : late TransitionDelegate _currentTransitionDelegate;
253 :
254 : /// Which location to pop to, instead of default pop.
255 : ///
256 : /// This is more general than [_beamBackOnPop].
257 : RouteInformation? _popConfiguration;
258 :
259 : /// Whether all the pages from [currentBeamLocation] are stacked.
260 : /// If not (`false`), just the last page is taken.
261 : bool _stacked = true;
262 :
263 : /// If `false`, does not report the route until next [update].
264 : ///
265 : /// Useful when having sibling beamers that are both build at the same time.
266 : /// Becomes active on next [update].
267 : bool active = true;
268 :
269 : /// The [Navigator] that belongs to this [BeamerDelegate].
270 : ///
271 : /// Useful for popping dialogs without accessing [BuildContext]:
272 : ///
273 : /// ```dart
274 : /// beamerDelegate.navigator.pop();
275 : /// ```
276 3 : NavigatorState get navigator => _navigatorKey.currentState!;
277 :
278 : /// Main method to update the [state] of this; `Beamer.of(context)`,
279 : ///
280 : /// This "top-level" [update] is generally used for navigation
281 : /// _between_ [BeamLocation]s and not _within_ a specific [BeamLocation].
282 : /// For latter purpose, see [BeamLocation.update].
283 : /// Nevertheless, [update] **will** work for navigation within [BeamLocation].
284 : /// Calling [update] will run the [locationBuilder].
285 : ///
286 : /// ```dart
287 : /// Beamer.of(context).update(
288 : /// state: BeamState.fromUriString('/xx'),
289 : /// );
290 : /// ```
291 : ///
292 : /// **[beamTo] and [beamToNamed] call [update] to really do the update.**
293 : ///
294 : /// [transitionDelegate] determines how a new stack of pages replaces current.
295 : /// See [Navigator.transitionDelegate].
296 : ///
297 : /// If [beamBackOnPop] is set to `true`,
298 : /// default pop action will triger [beamBack] instead.
299 : ///
300 : /// [popState] is more general than [beamBackOnPop],
301 : /// and can beam you anywhere; whatever it resolves to during build.
302 : ///
303 : /// If [stacked] is set to `false`,
304 : /// only the location's last page will be shown.
305 : ///
306 : /// If [replaceCurrent] is set to `true`,
307 : /// new location will replace the last one in the stack.
308 : ///
309 : /// If [rebuild] is set to `false`,
310 : /// [build] will not occur, but [state] and browser URL will be updated.
311 6 : void update({
312 : RouteInformation? configuration,
313 : RouteInformation? popConfiguration,
314 : TransitionDelegate? transitionDelegate,
315 : bool beamBackOnPop = false,
316 : bool popBeamLocationOnPop = false,
317 : bool stacked = true,
318 : bool replaceCurrent = false,
319 : bool buildBeamLocation = true,
320 : bool rebuild = true,
321 : bool updateParent = true,
322 : }) {
323 6 : configuration = configuration?.copyWith(
324 12 : location: Utils.trimmed(configuration.location),
325 : );
326 1 : popConfiguration = popConfiguration?.copyWith(
327 2 : location: Utils.trimmed(popConfiguration.location),
328 : );
329 :
330 6 : active = true;
331 12 : _popConfiguration = popConfiguration ?? _popConfiguration;
332 12 : _currentTransitionDelegate = transitionDelegate ?? this.transitionDelegate;
333 6 : _beamBackOnPop = beamBackOnPop;
334 6 : _popBeamLocationOnPop = popBeamLocationOnPop;
335 6 : _stacked = stacked;
336 :
337 : if (configuration != null) {
338 6 : this.configuration = configuration;
339 : if (buildBeamLocation) {
340 12 : final location = locationBuilder(this.configuration);
341 6 : _pushHistory(location, replaceCurrent: replaceCurrent);
342 : }
343 6 : listener?.call(this.configuration, _currentBeamLocation);
344 : }
345 :
346 6 : if (this.updateParent &&
347 : updateParent &&
348 13 : configuration != _parent?.configuration) {
349 7 : _parent?.update(
350 2 : configuration: this.configuration.copyWith(),
351 : rebuild: false,
352 : );
353 : }
354 :
355 : if (!rebuild || !updateParent) {
356 8 : updateRouteInformation(this.configuration);
357 : }
358 :
359 : if (rebuild) {
360 6 : notifyListeners();
361 : }
362 : }
363 :
364 : /// {@template beamTo}
365 : /// Beams to a specific, manually configured [BeamLocation].
366 : ///
367 : /// For example
368 : /// ```dart
369 : /// Beamer.of(context).beamTo(
370 : /// Location2(
371 : /// BeamState(
372 : /// pathBlueprintSegments = ['user',':userId','transactions'],
373 : /// pathParameters = {'userId': '1'},
374 : /// queryParameters = {'perPage': '10'},
375 : /// data = {'favoriteUser': true},
376 : /// ),
377 : /// ),
378 : /// );
379 : /// ```
380 : ///
381 : /// See [update] for more details.
382 : /// {@endtemplate}
383 2 : void beamTo(
384 : BeamLocation location, {
385 : BeamLocation? popTo,
386 : TransitionDelegate? transitionDelegate,
387 : bool beamBackOnPop = false,
388 : bool popBeamLocationOnPop = false,
389 : bool stacked = true,
390 : bool replaceCurrent = false,
391 : }) {
392 2 : _pushHistory(location, replaceCurrent: replaceCurrent);
393 2 : update(
394 4 : configuration: location.state.routeInformation,
395 0 : popConfiguration: popTo?.state.routeInformation,
396 : transitionDelegate: transitionDelegate,
397 : beamBackOnPop: beamBackOnPop,
398 : popBeamLocationOnPop: popBeamLocationOnPop,
399 : stacked: stacked,
400 : buildBeamLocation: false,
401 : );
402 : }
403 :
404 : /// {@template beamToNamed}
405 : /// Beams to [BeamLocation] that has [uri] contained within its
406 : /// [BeamLocation.pathBlueprintSegments].
407 : ///
408 : /// For example
409 : /// ```dart
410 : /// Beamer.of(context).beamToNamed(
411 : /// '/user/1/transactions?perPage=10',
412 : /// data: {'favoriteUser': true},,
413 : /// );
414 : /// ```
415 : ///
416 : /// See [update] for more details.
417 : /// {@endtemplate}
418 6 : void beamToNamed(
419 : String uri, {
420 : Map<String, dynamic>? data,
421 : String? popToNamed,
422 : TransitionDelegate? transitionDelegate,
423 : bool beamBackOnPop = false,
424 : bool popBeamLocationOnPop = false,
425 : bool stacked = true,
426 : bool replaceCurrent = false,
427 : }) {
428 24 : final beamData = data ?? _currentBeamLocation.state.routeInformation.state;
429 6 : update(
430 6 : configuration: RouteInformation(location: uri, state: beamData),
431 : popConfiguration: popToNamed != null
432 1 : ? RouteInformation(location: popToNamed, state: beamData)
433 : : null,
434 : transitionDelegate: transitionDelegate,
435 : beamBackOnPop: beamBackOnPop,
436 : popBeamLocationOnPop: popBeamLocationOnPop,
437 : stacked: stacked,
438 : replaceCurrent: replaceCurrent,
439 : );
440 : }
441 :
442 : /// {@template popToNamed}
443 : /// Calls [beamToNamed] with a [ReverseTransitionDelegate].
444 : ///
445 : /// See [beamToNamed] for more details.
446 : /// {@endtemplate}
447 3 : void popToNamed(
448 : String uri, {
449 : Map<String, dynamic>? data,
450 : String? popToNamed,
451 : bool beamBackOnPop = false,
452 : bool popBeamLocationOnPop = false,
453 : bool stacked = true,
454 : bool replaceCurrent = false,
455 : }) {
456 6 : final index = routeHistory.lastIndexWhere(
457 9 : (element) => element.location == uri,
458 : );
459 6 : if (index != -1) {
460 4 : routeHistory.removeRange(index, routeHistory.length);
461 : }
462 3 : beamToNamed(
463 : uri,
464 : data: data,
465 : popToNamed: popToNamed,
466 : transitionDelegate: const ReverseTransitionDelegate(),
467 : beamBackOnPop: beamBackOnPop,
468 : popBeamLocationOnPop: popBeamLocationOnPop,
469 : stacked: stacked,
470 : replaceCurrent: replaceCurrent,
471 : );
472 : }
473 :
474 : /// {@template canBeamBack}
475 : /// Whether it is possible to [beamBack],
476 : /// i.e. there is more than 1 state in [routeHistory].
477 : /// {@endtemplate}
478 16 : bool get canBeamBack => routeHistory.length > 1;
479 :
480 : /// {@template beamBack}
481 : /// Beams to previous state in [routeHistory].
482 : /// and **removes** the last state from history.
483 : ///
484 : /// If there is no previous state, does nothing.
485 : ///
486 : /// Returns the success, whether the [state] updated.
487 : /// {@endtemplate}
488 4 : bool beamBack({Map<String, dynamic>? data}) {
489 4 : if (!canBeamBack) {
490 : return false;
491 : }
492 4 : removeLastRouteInformation();
493 : // has to exist because canbeamBack
494 4 : final lastConfiguration = removeLastRouteInformation()!;
495 4 : update(
496 4 : configuration: lastConfiguration.copyWith(state: data),
497 4 : transitionDelegate: beamBackTransitionDelegate,
498 : );
499 : return true;
500 : }
501 :
502 : /// Remove everything except last from [routeHistory].
503 1 : void clearRouteHistory() =>
504 5 : routeHistory.removeRange(0, routeHistory.length - 1);
505 :
506 : /// {@template canPopBeamLocation}
507 : /// Whether it is possible to [popBeamLocation],
508 : /// i.e. there is more than 1 location in [beamLocationHistory].
509 : /// {@endtemplate}
510 12 : bool get canPopBeamLocation => beamLocationHistory.length > 1;
511 :
512 : /// {@template popBeamLocation}
513 : /// Beams to previous location in [beamLocationHistory]
514 : /// and **removes** the last location from history.
515 : ///
516 : /// If there is no previous location, does nothing.
517 : ///
518 : /// Returns the success, whether the [currentBeamLocation] was changed.
519 : /// {@endtemplate}
520 3 : bool popBeamLocation() {
521 3 : if (!canPopBeamLocation) {
522 : return false;
523 : }
524 6 : _currentBeamLocation.removeListener(_updateFromLocation);
525 4 : beamLocationHistory.removeLast();
526 6 : _currentBeamLocation = beamLocationHistory.last;
527 12 : routeHistory.add(_currentBeamLocation.state.routeInformation.copyWith());
528 6 : _currentBeamLocation.addListener(_updateFromLocation);
529 2 : update(
530 2 : transitionDelegate: beamBackTransitionDelegate,
531 : );
532 : return true;
533 : }
534 :
535 : /// Remove everything except last from [beamLocationHistory].
536 1 : void clearBeamLocationHistory() =>
537 5 : beamLocationHistory.removeRange(0, beamLocationHistory.length - 1);
538 :
539 6 : @override
540 : RouteInformation? get currentConfiguration =>
541 24 : _parent == null ? _currentBeamLocation.state.routeInformation : null;
542 :
543 6 : @override
544 6 : GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
545 :
546 6 : @override
547 : Widget build(BuildContext context) {
548 18 : BeamGuard? guard = _checkGuards(guards, context, _currentBeamLocation);
549 : if (guard != null) {
550 1 : _applyGuard(guard, context);
551 : }
552 12 : if (_currentBeamLocation is NotFound) {
553 8 : if (notFoundRedirect == null && notFoundRedirectNamed == null) {
554 : // do nothing, pass on NotFound
555 : } else {
556 : late BeamLocation redirectBeamLocation;
557 1 : if (notFoundRedirect != null) {
558 1 : redirectBeamLocation = notFoundRedirect!;
559 1 : } else if (notFoundRedirectNamed != null) {
560 1 : redirectBeamLocation = locationBuilder(
561 2 : RouteInformation(location: notFoundRedirectNamed),
562 : );
563 : }
564 3 : _currentBeamLocation.removeListener(_updateFromLocation);
565 1 : _pushHistory(redirectBeamLocation);
566 1 : _updateFromLocation(rebuild: false);
567 : }
568 : }
569 :
570 6 : final navigator = Builder(
571 6 : builder: (context) {
572 12 : if (_currentBeamLocation is NotFound ||
573 12 : _currentBeamLocation is EmptyBeamLocation) {
574 12 : _currentPages = [notFoundPage!];
575 : } else {
576 12 : _currentPages = _stacked
577 12 : ? _currentBeamLocation.buildPages(
578 12 : context, _currentBeamLocation.state)
579 1 : : [
580 1 : _currentBeamLocation
581 3 : .buildPages(context, _currentBeamLocation.state)
582 1 : .last
583 : ];
584 : }
585 6 : if (active && kIsWeb && setBrowserTabTitle) {
586 0 : SystemChrome.setApplicationSwitcherDescription(
587 0 : ApplicationSwitcherDescription(
588 0 : label: _currentPages.last.title ??
589 0 : _currentBeamLocation.state.routeInformation.location,
590 0 : primaryColor: Theme.of(context).primaryColor.value,
591 : ));
592 : }
593 6 : return Navigator(
594 6 : key: navigatorKey,
595 6 : observers: navigatorObservers,
596 12 : transitionDelegate: _currentBeamLocation.transitionDelegate ??
597 6 : _currentTransitionDelegate,
598 1 : pages: guard != null && guard.showPage != null
599 0 : ? guard.replaceCurrentStack
600 0 : ? [guard.showPage!]
601 0 : : _currentPages + [guard.showPage!]
602 6 : : _currentPages,
603 3 : onPopPage: (route, result) {
604 3 : if (route.willHandlePopInternally) {
605 1 : if (!route.didPop(result)) {
606 : return false;
607 : }
608 : }
609 :
610 3 : if (_popConfiguration != null) {
611 1 : update(
612 1 : configuration: _popConfiguration,
613 1 : transitionDelegate: beamBackTransitionDelegate,
614 : replaceCurrent: true,
615 : );
616 3 : } else if (_popBeamLocationOnPop) {
617 1 : final didPopBeamLocation = popBeamLocation();
618 : if (!didPopBeamLocation) {
619 : return false;
620 : }
621 3 : } else if (_beamBackOnPop) {
622 1 : final didBeamBack = beamBack();
623 : if (!didBeamBack) {
624 : return false;
625 : }
626 : } else {
627 6 : final lastPage = _currentPages.last;
628 3 : if (lastPage is BeamPage) {
629 3 : if (lastPage.popToNamed != null) {
630 2 : popToNamed(lastPage.popToNamed!);
631 : } else {
632 3 : final shouldPop = lastPage.onPopPage(context, this, lastPage);
633 : if (!shouldPop) {
634 : return false;
635 : }
636 : }
637 : }
638 : }
639 :
640 3 : return route.didPop(result);
641 : },
642 : );
643 : },
644 : );
645 12 : return _currentBeamLocation.builder(context, navigator);
646 : }
647 :
648 6 : @override
649 : SynchronousFuture<void> setInitialRoutePath(RouteInformation configuration) {
650 12 : final uri = Uri.parse(configuration.location ?? '/');
651 12 : if (_currentBeamLocation is! EmptyBeamLocation) {
652 12 : configuration = _currentBeamLocation.state.routeInformation;
653 12 : } else if (uri.path == '/') {
654 6 : configuration = RouteInformation(
655 24 : location: initialPath + (uri.query.isNotEmpty ? '?${uri.query}' : ''),
656 : );
657 : }
658 6 : return setNewRoutePath(configuration);
659 : }
660 :
661 6 : @override
662 : SynchronousFuture<void> setNewRoutePath(RouteInformation configuration) {
663 6 : update(configuration: configuration);
664 6 : return SynchronousFuture(null);
665 : }
666 :
667 : /// Pass this call to [root] which notifies the platform for a [state] change.
668 : ///
669 : /// On Web, creates a new browser history entry and update URL
670 : ///
671 : /// See [SystemNavigator.routeInformationUpdated].
672 4 : void updateRouteInformation(RouteInformation routeInformation) {
673 4 : if (_parent == null) {
674 4 : SystemNavigator.routeInformationUpdated(
675 8 : location: configuration.location ?? '/',
676 8 : state: configuration.state,
677 : );
678 : } else {
679 6 : _parent!.updateRouteInformation(routeInformation);
680 : }
681 : }
682 :
683 6 : BeamGuard? _checkGuards(
684 : List<BeamGuard> guards,
685 : BuildContext context,
686 : BeamLocation location,
687 : ) {
688 13 : for (final guard in guards + location.guards) {
689 2 : if (guard.shouldGuard(location) && !guard.check(context, location)) {
690 1 : guard.onCheckFailed?.call(context, location);
691 : return guard;
692 : }
693 : }
694 : return null;
695 : }
696 :
697 1 : void _applyGuard(BeamGuard guard, BuildContext context) {
698 1 : if (guard.showPage != null) {
699 : return;
700 : }
701 :
702 : late BeamLocation redirectLocation;
703 :
704 2 : if (guard.beamTo == null && guard.beamToNamed == null) {
705 1 : final lastState = removeLastRouteInformation();
706 2 : configuration = lastState!.copyWith();
707 2 : redirectLocation = locationBuilder(configuration);
708 1 : } else if (guard.beamTo != null) {
709 1 : redirectLocation = guard.beamTo!(context);
710 1 : } else if (guard.beamToNamed != null) {
711 3 : configuration = RouteInformation(location: guard.beamToNamed!);
712 2 : redirectLocation = locationBuilder(configuration);
713 : }
714 :
715 2 : final anotherGuard = _checkGuards(guards, context, redirectLocation);
716 : if (anotherGuard != null) {
717 1 : return _applyGuard(anotherGuard, context);
718 : }
719 :
720 3 : _currentBeamLocation.removeListener(_updateFromLocation);
721 3 : if (guard.replaceCurrentStack && beamLocationHistory.isNotEmpty) {
722 1 : removeLastRouteInformation();
723 2 : beamLocationHistory.removeLast();
724 : }
725 1 : _pushHistory(redirectLocation);
726 1 : _updateFromLocation(rebuild: false);
727 : }
728 :
729 6 : void _pushHistory(BeamLocation location, {bool replaceCurrent = false}) {
730 12 : if (routeHistory.isEmpty ||
731 24 : routeHistory.last.location !=
732 18 : location.state.routeInformation.location) {
733 30 : routeHistory.add(location.state.routeInformation.copyWith());
734 : }
735 :
736 18 : _currentBeamLocation.removeListener(_updateFromLocation);
737 6 : if ((preferUpdate &&
738 24 : location.runtimeType == _currentBeamLocation.runtimeType ||
739 : replaceCurrent) &&
740 12 : beamLocationHistory.isNotEmpty) {
741 12 : beamLocationHistory.removeLast();
742 : }
743 6 : if (removeDuplicateHistory) {
744 6 : beamLocationHistory
745 26 : .removeWhere((l) => l.runtimeType == location.runtimeType);
746 : }
747 :
748 12 : beamLocationHistory.add(location);
749 18 : _currentBeamLocation = beamLocationHistory.last;
750 18 : _currentBeamLocation.addListener(_updateFromLocation);
751 : }
752 :
753 5 : RouteInformation? removeLastRouteInformation() {
754 10 : if (routeHistory.isEmpty) {
755 : return null;
756 : }
757 5 : if (updateParent) {
758 6 : _parent?.removeLastRouteInformation();
759 : }
760 10 : return routeHistory.removeLast();
761 : }
762 :
763 3 : void _initializeFromParent() {
764 12 : configuration = _parent!.configuration.copyWith();
765 6 : var location = locationBuilder(configuration);
766 3 : if (location is NotFound) {
767 0 : configuration = RouteInformation(location: initialPath);
768 0 : location = locationBuilder(configuration);
769 : }
770 3 : _pushHistory(location);
771 : }
772 :
773 3 : void _updateFromParent({bool rebuild = true}) {
774 3 : update(
775 9 : configuration: _parent!.configuration.copyWith(),
776 : rebuild: rebuild,
777 : updateParent: false,
778 : );
779 : }
780 :
781 3 : void _updateFromLocation({bool rebuild = true}) {
782 3 : update(
783 9 : configuration: _currentBeamLocation.state.routeInformation,
784 : buildBeamLocation: false,
785 : rebuild: rebuild,
786 : );
787 : }
788 :
789 0 : @override
790 : void dispose() {
791 0 : _parent?.removeListener(_updateFromParent);
792 0 : _currentBeamLocation.removeListener(_updateFromLocation);
793 0 : super.dispose();
794 : }
795 : }
|