LCOV - code coverage report
Current view: top level - src - beam_location.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 112 115 97.4 %
Date: 2021-12-03 10:03:44 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:beamer/beamer.dart';
       2             : import 'package:beamer/src/beam_state.dart';
       3             : import 'package:beamer/src/utils.dart';
       4             : import 'package:flutter/widgets.dart';
       5             : 
       6             : /// Parameters used while beaming.
       7             : class BeamParameters {
       8             :   /// Creates a [BeamParameters] with specified properties.
       9             :   ///
      10             :   /// All attributes can be null.
      11          18 :   const BeamParameters({
      12             :     this.transitionDelegate = const DefaultTransitionDelegate(),
      13             :     this.popConfiguration,
      14             :     this.beamBackOnPop = false,
      15             :     this.popBeamLocationOnPop = false,
      16             :     this.stacked = true,
      17             :   });
      18             : 
      19             :   /// Which transition delegate to use when building pages.
      20             :   final TransitionDelegate transitionDelegate;
      21             : 
      22             :   /// Which route to pop to, instead of default pop.
      23             :   ///
      24             :   /// This is more general than [beamBackOnPop].
      25             :   final RouteInformation? popConfiguration;
      26             : 
      27             :   /// Whether to implicitly [BeamerDelegate.beamBack] instead of default pop.
      28             :   final bool beamBackOnPop;
      29             : 
      30             :   /// Whether to remove entire current [BeamLocation] from history,
      31             :   /// instead of default pop.
      32             :   final bool popBeamLocationOnPop;
      33             : 
      34             :   /// Whether all the pages produced by [BeamLocation.buildPages] are stacked.
      35             :   /// If not (`false`), just the last page is taken.
      36             :   final bool stacked;
      37             : 
      38             :   /// Returns a copy of this with optional changes.
      39           6 :   BeamParameters copyWith({
      40             :     TransitionDelegate? transitionDelegate,
      41             :     RouteInformation? popConfiguration,
      42             :     bool? beamBackOnPop,
      43             :     bool? popBeamLocationOnPop,
      44             :     bool? stacked,
      45             :   }) {
      46           6 :     return BeamParameters(
      47           4 :       transitionDelegate: transitionDelegate ?? this.transitionDelegate,
      48           6 :       popConfiguration: popConfiguration ?? this.popConfiguration,
      49           5 :       beamBackOnPop: beamBackOnPop ?? this.beamBackOnPop,
      50           5 :       popBeamLocationOnPop: popBeamLocationOnPop ?? this.popBeamLocationOnPop,
      51           5 :       stacked: stacked ?? this.stacked,
      52             :     );
      53             :   }
      54             : }
      55             : 
      56             : /// An element of [BeamLocation.history] list.
      57             : ///
      58             : /// Contains the [RouteInformation] and [BeamParameters] at the moment
      59             : /// of beaming to it.
      60             : class HistoryElement {
      61             :   /// Creates a [HistoryElement] with specified properties.
      62             :   ///
      63             :   /// [routeInformation] must not be null.
      64           9 :   const HistoryElement(
      65             :     this.routeInformation, [
      66             :     this.parameters = const BeamParameters(),
      67             :   ]);
      68             : 
      69             :   /// A [RouteInformation] of this history entry.
      70             :   final RouteInformation routeInformation;
      71             : 
      72             :   /// Parameters that were used during beaming.
      73             :   final BeamParameters parameters;
      74             : }
      75             : 
      76             : /// Configuration for a navigatable application region.
      77             : ///
      78             : /// Responsible for
      79             : ///   * knowing which URIs it can handle: [pathPatterns]
      80             : ///   * knowing how to build a stack of pages: [buildPages]
      81             : ///   * keeping a [state] that provides the link between the first 2
      82             : ///
      83             : /// Extend this class to define your locations to which you can then beam to.
      84             : abstract class BeamLocation<T extends RouteInformationSerializable>
      85             :     extends ChangeNotifier {
      86             :   /// Creates a [BeamLocation] with specified properties.
      87             :   ///
      88             :   /// All attributes can be null.
      89           9 :   BeamLocation([
      90             :     RouteInformation? routeInformation,
      91             :     BeamParameters? beamParameters,
      92             :   ]) {
      93           9 :     create(routeInformation, beamParameters);
      94             :   }
      95             : 
      96             :   /// A state of this [BeamLocation].
      97             :   ///
      98             :   /// Upon beaming, it will be populated by all necessary attributes.
      99             :   /// See also: [BeamState].
     100             :   late T state;
     101             : 
     102             :   /// Beam parameters used to beam to the current [state].
     103           0 :   BeamParameters get beamParameters => history.last.parameters;
     104             : 
     105             :   /// An arbitrary data to be stored in this.
     106             :   /// This will persist while navigating within this [BeamLocation].
     107             :   ///
     108             :   /// Therefore, in the case of using [RoutesLocationBuilder] which uses only
     109             :   /// a single [RoutesBeamLocation] for all page stacks, this data will
     110             :   /// be available always, until overriden with some new data.
     111             :   Object? data;
     112             : 
     113             :   /// Whether [buildInit] was called.
     114             :   ///
     115             :   /// See [buildInit].
     116             :   bool mounted = false;
     117             : 
     118             :   /// Whether this [BeamLocation] is currently in use by [BeamerDelegate].
     119             :   ///
     120             :   /// This influences on the behavior of [create] that gets called on existing
     121             :   /// [BeamLocation]s when using [BeamerLocationBuilder] that uses [Utils.chooseBeamLocation].
     122             :   bool isCurrent = false;
     123             : 
     124             :   /// Creates the [state] and adds the [routeInformation] to [history].
     125             :   /// This is called only once during the lifetime of [BeamLocation].
     126             :   ///
     127             :   /// See [createState] and [addToHistory].
     128           9 :   void create([
     129             :     RouteInformation? routeInformation,
     130             :     BeamParameters? beamParameters,
     131             :     bool tryPoppingHistory = true,
     132             :   ]) {
     133           9 :     if (!isCurrent) {
     134             :       try {
     135           9 :         disposeState();
     136             :       } catch (e) {
     137             :         //
     138             :       }
     139          18 :       history.clear();
     140             :     }
     141          18 :     state = createState(
     142             :       routeInformation ?? const RouteInformation(location: '/'),
     143             :     );
     144           9 :     addToHistory(
     145          18 :       state.routeInformation,
     146             :       beamParameters ?? const BeamParameters(),
     147             :       tryPoppingHistory,
     148             :     );
     149             :   }
     150             : 
     151             :   /// How to create state from [RouteInformation] given by
     152             :   /// [BeamerDelegate] and passed via [BeamerDelegate.locationBuilder].
     153             :   ///
     154             :   /// This will be called only once during the lifetime of [BeamLocation].
     155             :   /// One should override this if using a custom state class.
     156             :   ///
     157             :   /// See [create].
     158           9 :   T createState(RouteInformation routeInformation) =>
     159           9 :       BeamState.fromRouteInformation(
     160             :         routeInformation,
     161             :         beamLocation: this,
     162             :       ) as T;
     163             : 
     164             :   /// What to do on state initalization.
     165             :   ///
     166             :   /// For example, add listeners to [state] if it's a [ChangeNotifier].
     167           6 :   void initState() {}
     168             : 
     169             :   /// Updates the [state] upon recieving new [RouteInformation], which usually
     170             :   /// happens after [BeamerDelegate.setNewRoutePath].
     171             :   ///
     172             :   /// Override this if you are using custom state whose copying
     173             :   /// should be handled customly.
     174             :   ///
     175             :   /// See [update].
     176           6 :   void updateState(RouteInformation routeInformation) {
     177          12 :     state = createState(routeInformation);
     178             :   }
     179             : 
     180             :   /// How to relase any resources used by [state].
     181             :   ///
     182             :   /// Override this if
     183             :   /// e.g. using a custom [ChangeNotifier] [state] to remove listeners.
     184           9 :   void disposeState() {}
     185             : 
     186             :   /// Updates the [state] and [history], depending on inputs.
     187             :   ///
     188             :   /// If [copy] function is provided, state should be created from given current [state].
     189             :   /// New [routeInformation] gets added to history.
     190             :   ///
     191             :   /// If [copy] is `null`, then [routeInformation] is used, either `null` or not.
     192             :   /// If [routeInformation] is `null`, then the state will upadate from
     193             :   /// last [history] element and nothing shall be added to [history].
     194             :   /// Else, the state updates from available [routeInformation].
     195             :   ///
     196             :   /// See [updateState] and [addToHistory].
     197           6 :   void update([
     198             :     T Function(T)? copy,
     199             :     RouteInformation? routeInformation,
     200             :     BeamParameters? beamParameters,
     201             :     bool rebuild = true,
     202             :     bool tryPoppingHistory = true,
     203             :   ]) {
     204             :     if (copy != null) {
     205           4 :       state = copy(state);
     206           2 :       addToHistory(
     207           4 :         state.routeInformation,
     208             :         beamParameters ?? const BeamParameters(),
     209             :         tryPoppingHistory,
     210             :       );
     211             :     } else {
     212             :       if (routeInformation == null) {
     213          16 :         updateState(history.last.routeInformation);
     214             :       } else {
     215           6 :         updateState(routeInformation);
     216           6 :         addToHistory(
     217          12 :           state.routeInformation,
     218             :           beamParameters ?? const BeamParameters(),
     219             :           tryPoppingHistory,
     220             :         );
     221             :       }
     222             :     }
     223             :     if (rebuild) {
     224           2 :       notifyListeners();
     225             :     }
     226             :   }
     227             : 
     228             :   /// The history of beaming for this.
     229             :   List<HistoryElement> history = [];
     230             : 
     231             :   /// Adds another [HistoryElement] to [history] list.
     232             :   /// The history element is created from given [state] and [beamParameters].
     233             :   ///
     234             :   /// If [tryPopping] is set to `true`, the state with the same `location`
     235             :   /// will be searched in [history] and if found, entire history segment
     236             :   /// `[foundIndex, history.length-1]` will be removed before adding a new
     237             :   /// history element.
     238           9 :   void addToHistory(
     239             :     RouteInformation routeInformation, [
     240             :     BeamParameters beamParameters = const BeamParameters(),
     241             :     bool tryPopping = true,
     242             :   ]) {
     243             :     if (tryPopping) {
     244          24 :       final sameStateIndex = history.indexWhere((element) {
     245          18 :         return element.routeInformation.location ==
     246          18 :             state.routeInformation.location;
     247             :       });
     248          18 :       if (sameStateIndex != -1) {
     249          16 :         history.removeRange(sameStateIndex, history.length);
     250             :       }
     251             :     }
     252          18 :     if (history.isEmpty ||
     253          36 :         routeInformation.location != history.last.routeInformation.location) {
     254          27 :       history.add(HistoryElement(routeInformation, beamParameters));
     255             :     }
     256             :   }
     257             : 
     258             :   /// Removes the last [HistoryElement] from [history] and returns it.
     259             :   ///
     260             :   /// If said history element is a `ChangeNotifier`, listeners are removed.
     261           4 :   HistoryElement? removeLastFromHistory() {
     262           8 :     if (history.isEmpty) {
     263             :       return null;
     264             :     }
     265           8 :     return history.removeLast();
     266             :   }
     267             : 
     268             :   /// Initialize custom bindings for this [BeamLocation] using [BuildContext].
     269             :   /// Similar to [builder], but is not tied to Widget tree.
     270             :   ///
     271             :   /// This will be called on just the first build of this [BeamLocation]
     272             :   /// and sets [mounted] to true. It is called right before [buildPages].
     273           6 :   @mustCallSuper
     274             :   void buildInit(BuildContext context) {
     275           6 :     mounted = true;
     276             :   }
     277             : 
     278             :   /// Can this handle the [uri] based on its [pathPatterns].
     279             :   ///
     280             :   /// Can be useful in a custom [BeamerDelegate.locationBuilder].
     281           2 :   bool canHandle(Uri uri) => Utils.canBeamLocationHandleUri(this, uri);
     282             : 
     283             :   /// Gives the ability to wrap the [navigator].
     284             :   ///
     285             :   /// Mostly useful for providing something to the entire location,
     286             :   /// i.e. to all of the pages.
     287             :   ///
     288             :   /// For example:
     289             :   ///
     290             :   /// ```dart
     291             :   /// @override
     292             :   /// Widget builder(BuildContext context, Widget navigator) {
     293             :   ///   return MyProvider<MyObject>(
     294             :   ///     create: (context) => MyObject(),
     295             :   ///     child: navigator,
     296             :   ///   );
     297             :   /// }
     298             :   /// ```
     299           6 :   Widget builder(BuildContext context, Widget navigator) => navigator;
     300             : 
     301             :   /// Represents the "form" of URI paths supported by this [BeamLocation].
     302             :   ///
     303             :   /// You can pass in either a String or a RegExp. Beware of using greedy regular
     304             :   /// expressions as this might lead to unexpected behaviour.
     305             :   ///
     306             :   /// For strings, optional path segments are denoted with ':xxx' and consequently
     307             :   /// `{'xxx': <real>}` will be put to [pathParameters].
     308             :   /// For regular expressions we use named groups as optional path segments, following
     309             :   /// regex is tested to be effective in most cases `RegExp('/test/(?<test>[a-z]+){0,1}')`
     310             :   /// This will put `{'test': <real>}` to [pathParameters]. Note that we use the name from the regex group.
     311             :   ///
     312             :   /// Optional path segments can be used as a mean to pass data regardless of
     313             :   /// whether there is a browser.
     314             :   ///
     315             :   /// For example: '/books/:id' or using regex `RegExp('/test/(?<test>[a-z]+){0,1}')`
     316             :   List<Pattern> get pathPatterns;
     317             : 
     318             :   /// Creates and returns the list of pages to be built by the [Navigator]
     319             :   /// when this [BeamLocation] is beamed to or internally inferred.
     320             :   ///
     321             :   /// [context] can be useful while building the pages.
     322             :   /// It will also contain anything injected via [builder].
     323             :   List<BeamPage> buildPages(BuildContext context, T state);
     324             : 
     325             :   /// Guards that will be executing [check] when this gets beamed to.
     326             :   ///
     327             :   /// Checks will be executed in order; chain of responsibility pattern.
     328             :   /// When some guard returns `false`, a candidate will not be accepted
     329             :   /// and stack of pages will be updated as is configured in [BeamGuard].
     330             :   ///
     331             :   /// Override this in your subclasses, if needed.
     332             :   /// See [BeamGuard].
     333           6 :   List<BeamGuard> get guards => const <BeamGuard>[];
     334             : 
     335             :   /// A transition delegate to be used by [Navigator].
     336             :   ///
     337             :   /// This will be used only by this location, unlike
     338             :   /// [BeamerDelegate.transitionDelegate] that will be used for all locations.
     339             :   ///
     340             :   /// This transition delegate will override the one in [BeamerDelegate].
     341             :   ///
     342             :   /// See [Navigator.transitionDelegate].
     343           6 :   TransitionDelegate? get transitionDelegate => null;
     344             : }
     345             : 
     346             : /// Default location to choose if requested URI doesn't parse to any location.
     347             : class NotFound extends BeamLocation<BeamState> {
     348             :   /// Creates a [NotFound] [BeamLocation] with
     349             :   /// `RouteInformation(location: path)` as its state.
     350          18 :   NotFound({String path = '/'}) : super(RouteInformation(location: path));
     351             : 
     352           1 :   @override
     353           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     354             : 
     355           6 :   @override
     356           6 :   List<String> get pathPatterns => [];
     357             : }
     358             : 
     359             : /// Empty location used to intialize a non-nullable BeamLocation variable.
     360             : ///
     361             : /// See [BeamerDelegate.currentBeamLocation].
     362             : class EmptyBeamLocation extends BeamLocation<BeamState> {
     363           1 :   @override
     364           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     365             : 
     366           7 :   @override
     367           7 :   List<String> get pathPatterns => [];
     368             : }
     369             : 
     370             : /// A beam location for [RoutesLocationBuilder], but can be used freely.
     371             : ///
     372             : /// Useful when needing a simple beam location with a single or few pages.
     373             : class RoutesBeamLocation extends BeamLocation<BeamState> {
     374             :   /// Creates a [RoutesBeamLocation] with specified properties.
     375             :   ///
     376             :   /// [routeInformation] and [routes] are required.
     377           6 :   RoutesBeamLocation({
     378             :     required RouteInformation routeInformation,
     379             :     Object? data,
     380             :     BeamParameters? beamParameters,
     381             :     required this.routes,
     382             :     this.navBuilder,
     383           6 :   }) : super(routeInformation, beamParameters);
     384             : 
     385             :   /// Map of all routes this location handles.
     386             :   Map<Pattern, dynamic Function(BuildContext, BeamState, Object? data)> routes;
     387             : 
     388             :   /// A wrapper used as [BeamLocation.builder].
     389             :   Widget Function(BuildContext context, Widget navigator)? navBuilder;
     390             : 
     391           6 :   @override
     392             :   Widget builder(BuildContext context, Widget navigator) {
     393           6 :     return navBuilder?.call(context, navigator) ?? navigator;
     394             :   }
     395             : 
     396           5 :   int _compareKeys(Pattern a, Pattern b) {
     397           5 :     if (a is RegExp && b is RegExp) {
     398           0 :       return a.pattern.length - b.pattern.length;
     399             :     }
     400           5 :     if (a is RegExp && b is String) {
     401           0 :       return a.pattern.length - b.length;
     402             :     }
     403          10 :     if (a is String && b is RegExp) {
     404           4 :       return a.length - b.pattern.length;
     405             :     }
     406          10 :     if (a is String && b is String) {
     407          15 :       return a.length - b.length;
     408             :     }
     409             :     return 0;
     410             :   }
     411             : 
     412           6 :   @override
     413          18 :   List<Pattern> get pathPatterns => routes.keys.toList();
     414             : 
     415           6 :   @override
     416             :   List<BeamPage> buildPages(BuildContext context, BeamState state) {
     417          24 :     final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys);
     418          12 :     final routeBuilders = Map.of(routes)
     419          18 :       ..removeWhere((key, value) => !filteredRoutes.containsKey(key));
     420          12 :     final sortedRoutes = routeBuilders.keys.toList()
     421          16 :       ..sort((a, b) => _compareKeys(a, b));
     422          12 :     final pages = sortedRoutes.map<BeamPage>((route) {
     423          18 :       final routeElement = routes[route]!(context, state, data);
     424           6 :       if (routeElement is BeamPage) {
     425             :         return routeElement;
     426             :       } else {
     427           6 :         return BeamPage(
     428          12 :           key: ValueKey(filteredRoutes[route]),
     429             :           child: routeElement,
     430             :         );
     431             :       }
     432           6 :     }).toList();
     433             :     return pages;
     434             :   }
     435             : 
     436             :   /// Chooses all the routes that "sub-match" [state.routeInformation] to stack their pages.
     437             :   ///
     438             :   /// If none of the routes _matches_ [state.uri], nothing will be selected
     439             :   /// and [BeamerDelegate] will declare that the location is [NotFound].
     440           6 :   static Map<Pattern, String> chooseRoutes(
     441             :     RouteInformation routeInformation,
     442             :     Iterable<Pattern> routes,
     443             :   ) {
     444           6 :     final matched = <Pattern, String>{};
     445             :     bool overrideNotFound = false;
     446          12 :     final uri = Uri.parse(routeInformation.location ?? '/');
     447          12 :     for (final route in routes) {
     448           6 :       if (route is String) {
     449          12 :         final uriPathSegments = uri.pathSegments.toList();
     450          12 :         final routePathSegments = Uri.parse(route).pathSegments;
     451             : 
     452          18 :         if (uriPathSegments.length < routePathSegments.length) {
     453             :           continue;
     454             :         }
     455             : 
     456             :         var checksPassed = true;
     457             :         var path = '';
     458          18 :         for (int i = 0; i < routePathSegments.length; i++) {
     459          18 :           path += '/${uriPathSegments[i]}';
     460             : 
     461          12 :           if (routePathSegments[i] == '*') {
     462             :             overrideNotFound = true;
     463             :             continue;
     464             :           }
     465          12 :           if (routePathSegments[i].startsWith(':')) {
     466             :             continue;
     467             :           }
     468          18 :           if (routePathSegments[i] != uriPathSegments[i]) {
     469             :             checksPassed = false;
     470             :             break;
     471             :           }
     472             :         }
     473             : 
     474             :         if (checksPassed) {
     475          12 :           matched[route] = Uri(
     476           6 :             path: path == '' ? '/' : path,
     477             :             queryParameters:
     478          14 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     479           6 :           ).toString();
     480             :         }
     481             :       } else {
     482           1 :         final regexp = Utils.tryCastToRegExp(route);
     483           2 :         if (regexp.hasMatch(uri.toString())) {
     484           1 :           final path = uri.toString();
     485           2 :           matched[regexp] = Uri(
     486           1 :             path: path == '' ? '/' : path,
     487             :             queryParameters:
     488           2 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     489           1 :           ).toString();
     490             :         }
     491             :       }
     492             :     }
     493             : 
     494             :     bool isNotFound = true;
     495          12 :     matched.forEach((key, value) {
     496           6 :       if (Utils.urisMatch(key, uri)) {
     497             :         isNotFound = false;
     498             :       }
     499             :     });
     500             : 
     501             :     if (overrideNotFound) {
     502             :       return matched;
     503             :     }
     504             : 
     505           3 :     return isNotFound ? {} : matched;
     506             :   }
     507             : }

Generated by: LCOV version 1.14