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