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