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

Generated by: LCOV version 1.14