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