LCOV - code coverage report
Current view: top level - src - beam_location.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 104 109 95.4 %
Date: 2021-09-11 22:34:03 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          19 :   const BeamParameters({
       9             :     this.transitionDelegate = const DefaultTransitionDelegate(),
      10             :     this.popConfiguration,
      11             :     this.beamBackOnPop = false,
      12             :     this.popBeamLocationOnPop = false,
      13             :     this.stacked = true,
      14             :   });
      15             : 
      16             :   /// Which transition delegate to use when building pages.
      17             :   final TransitionDelegate transitionDelegate;
      18             : 
      19             :   /// Which route to pop to, instead of default pop.
      20             :   ///
      21             :   /// This is more general than [beamBackOnPop].
      22             :   final RouteInformation? popConfiguration;
      23             : 
      24             :   /// Whether to implicitly [BeamerDelegate.beamBack] instead of default pop.
      25             :   final bool beamBackOnPop;
      26             : 
      27             :   /// Whether to remove entire current [BeamLocation] from history,
      28             :   /// instead of default pop.
      29             :   final bool popBeamLocationOnPop;
      30             : 
      31             :   /// Whether all the pages produced by [BeamLocation.buildPages] are stacked.
      32             :   /// If not (`false`), just the last page is taken.
      33             :   final bool stacked;
      34             : 
      35             :   /// Returns a copy of this with optional changes.
      36           6 :   BeamParameters copyWith({
      37             :     TransitionDelegate? transitionDelegate,
      38             :     RouteInformation? popConfiguration,
      39             :     bool? beamBackOnPop,
      40             :     bool? popBeamLocationOnPop,
      41             :     bool? stacked,
      42             :   }) {
      43           6 :     return BeamParameters(
      44           4 :       transitionDelegate: transitionDelegate ?? this.transitionDelegate,
      45           6 :       popConfiguration: popConfiguration ?? this.popConfiguration,
      46           5 :       beamBackOnPop: beamBackOnPop ?? this.beamBackOnPop,
      47           5 :       popBeamLocationOnPop: popBeamLocationOnPop ?? this.popBeamLocationOnPop,
      48           5 :       stacked: stacked ?? this.stacked,
      49             :     );
      50             :   }
      51             : }
      52             : 
      53             : /// An element of [BeamLocation.history] list.
      54             : ///
      55             : /// Contains the [BeamLocation.state] and [BeamParameters] at the moment
      56             : /// of beaming to mentioned state.
      57             : class HistoryElement<T extends RouteInformationSerializable> {
      58           9 :   const HistoryElement(
      59             :     this.state, [
      60             :     this.parameters = const BeamParameters(),
      61             :   ]);
      62             : 
      63             :   final T state;
      64             :   final BeamParameters parameters;
      65             : }
      66             : 
      67             : /// Configuration for a navigatable application region.
      68             : ///
      69             : /// Responsible for
      70             : ///   * knowing which URIs it can handle: [pathPatterns]
      71             : ///   * knowing how to build a stack of pages: [buildPages]
      72             : ///   * keeping a [state] that provides the link between the first 2
      73             : ///
      74             : /// Extend this class to define your locations to which you can then beam to.
      75             : abstract class BeamLocation<T extends RouteInformationSerializable>
      76             :     extends ChangeNotifier {
      77           9 :   BeamLocation([
      78             :     RouteInformation? routeInformation,
      79             :     BeamParameters? beamParameters,
      80             :   ]) {
      81           9 :     addToHistory(
      82           9 :       createState(
      83             :         routeInformation ?? const RouteInformation(location: '/'),
      84             :       ),
      85             :       beamParameters ?? const BeamParameters(),
      86             :     );
      87             :   }
      88             : 
      89             :   /// A state of this location.
      90             :   ///
      91             :   /// Upon beaming, it will be populated by all necessary attributes.
      92             :   /// See also: [BeamState].
      93          32 :   T get state => history.last.state;
      94             : 
      95           1 :   set state(T state) =>
      96           6 :       history.last = HistoryElement(state, history.last.parameters);
      97             : 
      98             :   /// An arbitrary data to be stored in this.
      99             :   /// This will persist while navigating through this [BeamLocation].
     100             :   ///
     101             :   /// Therefore, in the case of using [RoutesLocationBuilder] which uses only
     102             :   /// a single [RoutesBeamLocation] for all page stacks, this data will
     103             :   /// be available always, until overriden with some new data.
     104             :   Object? data;
     105             : 
     106             :   /// Beam parameters used to beam to the [state].
     107          24 :   BeamParameters get beamParameters => history.last.parameters;
     108             : 
     109             :   /// How to create state from generic [BeamState], that is produced
     110             :   /// by [BeamerDelegate] and passed via [BeamerDelegate.locationBuilder].
     111             :   ///
     112             :   /// Override this if you have your custom state class extending [BeamState].
     113           9 :   T createState(RouteInformation routeInformation) =>
     114           9 :       BeamState.fromRouteInformation(
     115             :         routeInformation,
     116             :         beamLocation: this,
     117             :       ) as T;
     118             : 
     119             :   /// Update a state via callback receiving the current state.
     120             :   /// If no callback is given, just notifies [BeamerDelegate] to rebuild.
     121             :   ///
     122             :   /// Useful with [BeamState.copyWith].
     123           2 :   void update([
     124             :     T Function(T)? copy,
     125             :     BeamParameters? beamParameters,
     126             :     bool rebuild = true,
     127             :     bool tryPoppingHistory = true,
     128             :   ]) {
     129             :     if (copy != null) {
     130           2 :       addToHistory(
     131           2 :         copy(state),
     132             :         beamParameters ?? const BeamParameters(),
     133             :         tryPoppingHistory,
     134             :       );
     135             :     }
     136             :     if (rebuild) {
     137           2 :       notifyListeners();
     138             :     }
     139             :   }
     140             : 
     141             :   /// The history of beaming for this.
     142             :   final List<HistoryElement<T>> history = [];
     143             : 
     144           9 :   void addToHistory(
     145             :     RouteInformationSerializable state, [
     146             :     BeamParameters beamParameters = const BeamParameters(),
     147             :     bool tryPopping = true,
     148             :   ]) {
     149             :     if (tryPopping) {
     150          25 :       final sameStateIndex = history.indexWhere((element) {
     151          28 :         return element.state.routeInformation.location ==
     152          14 :             state.routeInformation.location;
     153             :       });
     154          18 :       if (sameStateIndex != -1) {
     155          20 :         for (int i = sameStateIndex; i < history.length; i++) {
     156          15 :           if (history[i] is ChangeNotifier) {
     157           0 :             (history[i] as ChangeNotifier).removeListener(update);
     158             :           }
     159             :         }
     160          20 :         history.removeRange(sameStateIndex, history.length);
     161             :       }
     162             :     }
     163          18 :     if (history.isEmpty ||
     164          21 :         state.routeInformation.location !=
     165          21 :             this.state.routeInformation.location) {
     166           9 :       if (state is ChangeNotifier) {
     167           0 :         (state as ChangeNotifier).addListener(update);
     168             :       }
     169          27 :       history.add(HistoryElement<T>(state as T, beamParameters));
     170             :     }
     171             :   }
     172             : 
     173           4 :   HistoryElement? removeLastFromHistory() {
     174           8 :     if (history.isEmpty) {
     175             :       return null;
     176             :     }
     177           8 :     final last = history.removeLast();
     178           4 :     if (last is ChangeNotifier) {
     179           0 :       (last as ChangeNotifier).removeListener(update);
     180             :     }
     181             :     return last;
     182             :   }
     183             : 
     184             :   /// Can this handle the [uri] based on its [pathPatterns].
     185             :   ///
     186             :   /// Can be useful in a custom [BeamerDelegate.locationBuilder].
     187           2 :   bool canHandle(Uri uri) => Utils.canBeamLocationHandleUri(this, uri);
     188             : 
     189             :   /// Gives the ability to wrap the [navigator].
     190             :   ///
     191             :   /// Mostly useful for providing something to the entire location,
     192             :   /// i.e. to all of the pages.
     193             :   ///
     194             :   /// For example:
     195             :   ///
     196             :   /// ```dart
     197             :   /// @override
     198             :   /// Widget builder(BuildContext context, Widget navigator) {
     199             :   ///   return MyProvider<MyObject>(
     200             :   ///     create: (context) => MyObject(),
     201             :   ///     child: navigator,
     202             :   ///   );
     203             :   /// }
     204             :   /// ```
     205           6 :   Widget builder(BuildContext context, Widget navigator) => navigator;
     206             : 
     207             :   /// Represents the "form" of URI paths supported by this [BeamLocation].
     208             :   ///
     209             :   /// You can pass in either a String or a RegExp. Beware of using greedy regular
     210             :   /// expressions as this might lead to unexpected behaviour.
     211             :   ///
     212             :   /// For strings, optional path segments are denoted with ':xxx' and consequently
     213             :   /// `{'xxx': <real>}` will be put to [pathParameters].
     214             :   /// For regular expressions we use named groups as optional path segments, following
     215             :   /// regex is tested to be effective in most cases `RegExp('/test/(?<test>[a-z]+){0,1}')`
     216             :   /// This will put `{'test': <real>}` to [pathParameters]. Note that we use the name from the regex group.
     217             :   ///
     218             :   /// Optional path segments can be used as a mean to pass data regardless of
     219             :   /// whether there is a browser.
     220             :   ///
     221             :   /// For example: '/books/:id' or using regex `RegExp('/test/(?<test>[a-z]+){0,1}')`
     222             :   List<Pattern> get pathPatterns;
     223             : 
     224             :   /// Creates and returns the list of pages to be built by the [Navigator]
     225             :   /// when this [BeamLocation] is beamed to or internally inferred.
     226             :   ///
     227             :   /// [context] can be useful while building the pages.
     228             :   /// It will also contain anything injected via [builder].
     229             :   List<BeamPage> buildPages(BuildContext context, T state);
     230             : 
     231             :   /// Guards that will be executing [check] when this gets beamed to.
     232             :   ///
     233             :   /// Checks will be executed in order; chain of responsibility pattern.
     234             :   /// When some guard returns `false`, a candidate will not be accepted
     235             :   /// and stack of pages will be updated as is configured in [BeamGuard].
     236             :   ///
     237             :   /// Override this in your subclasses, if needed.
     238             :   /// See [BeamGuard].
     239           6 :   List<BeamGuard> get guards => const <BeamGuard>[];
     240             : 
     241             :   /// A transition delegate to be used by [Navigator].
     242             :   ///
     243             :   /// This will be used only by this location, unlike
     244             :   /// [BeamerDelegate.transitionDelegate] that will be used for all locations.
     245             :   ///
     246             :   /// This transition delegate will override the one in [BeamerDelegate].
     247             :   ///
     248             :   /// See [Navigator.transitionDelegate].
     249           6 :   TransitionDelegate? get transitionDelegate => null;
     250             : }
     251             : 
     252             : /// Default location to choose if requested URI doesn't parse to any location.
     253             : class NotFound extends BeamLocation<BeamState> {
     254          18 :   NotFound({String path = '/'}) : super(RouteInformation(location: path));
     255             : 
     256           1 :   @override
     257           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     258             : 
     259           6 :   @override
     260           6 :   List<String> get pathPatterns => [];
     261             : }
     262             : 
     263             : /// Empty location used to intialize a non-nullable BeamLocation variable.
     264             : ///
     265             : /// See [BeamerDelegate.currentBeamLocation].
     266             : class EmptyBeamLocation extends BeamLocation<BeamState> {
     267           1 :   @override
     268           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     269             : 
     270           7 :   @override
     271           7 :   List<String> get pathPatterns => [];
     272             : }
     273             : 
     274             : /// A beam location for [RoutesLocationBuilder], but can be used freely.
     275             : ///
     276             : /// Useful when needing a simple beam location with a single or few pages.
     277             : class RoutesBeamLocation extends BeamLocation<BeamState> {
     278           6 :   RoutesBeamLocation({
     279             :     required RouteInformation routeInformation,
     280             :     Object? data,
     281             :     BeamParameters? beamParameters,
     282             :     required this.routes,
     283             :     this.navBuilder,
     284           6 :   }) : super(routeInformation, beamParameters);
     285             : 
     286             :   /// Map of all routes this location handles.
     287             :   Map<Pattern, dynamic Function(BuildContext, BeamState)> routes;
     288             : 
     289             :   /// A wrapper used as [BeamLocation.builder].
     290             :   Widget Function(BuildContext context, Widget navigator)? navBuilder;
     291             : 
     292           6 :   @override
     293             :   Widget builder(BuildContext context, Widget navigator) {
     294           6 :     return navBuilder?.call(context, navigator) ?? navigator;
     295             :   }
     296             : 
     297           5 :   int _compareKeys(Pattern a, Pattern b) {
     298           5 :     if (a is RegExp && b is RegExp) {
     299           0 :       return a.pattern.length - b.pattern.length;
     300             :     }
     301           5 :     if (a is RegExp && b is String) {
     302           0 :       return a.pattern.length - b.length;
     303             :     }
     304          10 :     if (a is String && b is RegExp) {
     305           4 :       return a.length - b.pattern.length;
     306             :     }
     307          10 :     if (a is String && b is String) {
     308          15 :       return a.length - b.length;
     309             :     }
     310             :     return 0;
     311             :   }
     312             : 
     313           6 :   @override
     314          18 :   List<Pattern> get pathPatterns => routes.keys.toList();
     315             : 
     316           6 :   @override
     317             :   List<BeamPage> buildPages(BuildContext context, BeamState state) {
     318          24 :     final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys);
     319          12 :     final routeBuilders = Map.of(routes)
     320          18 :       ..removeWhere((key, value) => !filteredRoutes.containsKey(key));
     321          12 :     final sortedRoutes = routeBuilders.keys.toList()
     322          16 :       ..sort((a, b) => _compareKeys(a, b));
     323          12 :     final pages = sortedRoutes.map<BeamPage>((route) {
     324          12 :       final routeElement = routes[route]!(context, state);
     325           6 :       if (routeElement is BeamPage) {
     326             :         return routeElement;
     327             :       } else {
     328           6 :         return BeamPage(
     329          12 :           key: ValueKey(filteredRoutes[route]),
     330             :           child: routeElement,
     331             :         );
     332             :       }
     333           6 :     }).toList();
     334             :     return pages;
     335             :   }
     336             : 
     337             :   /// Chooses all the routes that "sub-match" [state.routeInformation] to stack their pages.
     338             :   ///
     339             :   /// If none of the routes _matches_ [state.uri], nothing will be selected
     340             :   /// and [BeamerDelegate] will declare that the location is [NotFound].
     341           6 :   static Map<Pattern, String> chooseRoutes(
     342             :     RouteInformation routeInformation,
     343             :     Iterable<Pattern> routes,
     344             :   ) {
     345           6 :     final matched = <Pattern, String>{};
     346             :     bool overrideNotFound = false;
     347          12 :     final uri = Uri.parse(routeInformation.location ?? '/');
     348          12 :     for (final route in routes) {
     349           6 :       if (route is String) {
     350          12 :         final uriPathSegments = uri.pathSegments.toList();
     351          12 :         final routePathSegments = Uri.parse(route).pathSegments;
     352             : 
     353          18 :         if (uriPathSegments.length < routePathSegments.length) {
     354             :           continue;
     355             :         }
     356             : 
     357             :         var checksPassed = true;
     358             :         var path = '';
     359          18 :         for (int i = 0; i < routePathSegments.length; i++) {
     360          18 :           path += '/${uriPathSegments[i]}';
     361             : 
     362          12 :           if (routePathSegments[i] == '*') {
     363             :             overrideNotFound = true;
     364             :             continue;
     365             :           }
     366          12 :           if (routePathSegments[i].startsWith(':')) {
     367             :             continue;
     368             :           }
     369          18 :           if (routePathSegments[i] != uriPathSegments[i]) {
     370             :             checksPassed = false;
     371             :             break;
     372             :           }
     373             :         }
     374             : 
     375             :         if (checksPassed) {
     376          12 :           matched[route] = Uri(
     377           6 :             path: path == '' ? '/' : path,
     378             :             queryParameters:
     379          13 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     380           6 :           ).toString();
     381             :         }
     382             :       } else {
     383           1 :         final regexp = Utils.tryCastToRegExp(route);
     384           2 :         if (regexp.hasMatch(uri.toString())) {
     385           1 :           final path = uri.toString();
     386           2 :           matched[regexp] = Uri(
     387           1 :             path: path == '' ? '/' : path,
     388             :             queryParameters:
     389           2 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     390           1 :           ).toString();
     391             :         }
     392             :       }
     393             :     }
     394             : 
     395             :     bool isNotFound = true;
     396          12 :     matched.forEach((key, value) {
     397           6 :       if (Utils.urisMatch(key, uri)) {
     398             :         isNotFound = false;
     399             :       }
     400             :     });
     401             : 
     402             :     if (overrideNotFound) {
     403             :       return matched;
     404             :     }
     405             : 
     406           3 :     return isNotFound ? {} : matched;
     407             :   }
     408             : }

Generated by: LCOV version 1.14