LCOV - code coverage report
Current view: top level - src - beam_location.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 68 68 100.0 %
Date: 2021-07-18 19:09:57 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             : /// Configuration for a navigatable application region.
       7             : ///
       8             : /// Responsible for
       9             : ///   * knowing which URIs it can handle: [pathBlueprints]
      10             : ///   * knowing how to build a stack of pages: [buildPages]
      11             : ///   * keeping a [state] that provides the link between the first 2
      12             : ///
      13             : /// Extend this class to define your locations to which you can then beam to.
      14             : abstract class BeamLocation<T extends RouteInformationSerializable>
      15             :     extends ChangeNotifier {
      16           9 :   BeamLocation([RouteInformation? routeInformation]) {
      17          18 :     state = createState(
      18             :       routeInformation ?? const RouteInformation(location: '/'),
      19             :     );
      20             :   }
      21             : 
      22             :   /// A state of this location.
      23             :   ///
      24             :   /// Upon beaming, it will be populated by all necessary attributes.
      25             :   /// See [BeamState].
      26             :   late T state;
      27             : 
      28             :   /// How to create state from generic [BeamState], that is produced
      29             :   /// by [BeamerDelegate] and passed via [BeamerDelegate.locationBuilder].
      30             :   ///
      31             :   /// Override this if you have your custom state class extending [BeamState].
      32           9 :   T createState(RouteInformation routeInformation) =>
      33           9 :       BeamState.fromRouteInformation(routeInformation, beamLocation: this) as T;
      34             : 
      35             :   /// Update a state via callback receiving the current state.
      36             :   /// If no callback is given, just notifies [BeamerDelegate] to rebuild.
      37             :   ///
      38             :   /// Useful with [BeamState.copyWith].
      39           2 :   void update([T Function(T)? copy, bool rebuild = true]) {
      40             :     if (copy != null) {
      41           4 :       state = copy(state);
      42             :     }
      43             :     if (rebuild) {
      44           2 :       notifyListeners();
      45             :     }
      46             :   }
      47             : 
      48             :   /// Can this handle the [uri] based on its [pathBlueprints].
      49             :   ///
      50             :   /// Can be useful in a custom [BeamerDelegate.locationBuilder].
      51           2 :   bool canHandle(Uri uri) => Utils.canBeamLocationHandleUri(this, uri);
      52             : 
      53             :   /// Gives the ability to wrap the [navigator].
      54             :   ///
      55             :   /// Mostly useful for providing something to the entire location,
      56             :   /// i.e. to all of the pages.
      57             :   ///
      58             :   /// For example:
      59             :   ///
      60             :   /// ```dart
      61             :   /// @override
      62             :   /// Widget builder(BuildContext context, Widget navigator) {
      63             :   ///   return MyProvider<MyObject>(
      64             :   ///     create: (context) => MyObject(),
      65             :   ///     child: navigator,
      66             :   ///   );
      67             :   /// }
      68             :   /// ```
      69           6 :   Widget builder(BuildContext context, Widget navigator) => navigator;
      70             : 
      71             :   /// Represents the "form" of URI paths supported by this [BeamLocation].
      72             :   ///
      73             :   /// You can pass in either a String or a RegExp. Beware of using greedy regular
      74             :   /// expressions as this might lead to unexpected behaviour.
      75             :   ///
      76             :   /// For strings, optional path segments are denoted with ':xxx' and consequently
      77             :   /// `{'xxx': <real>}` will be put to [pathParameters].
      78             :   /// For regular expressions we use named groups as optional path segments, following
      79             :   /// regex is tested to be effective in most cases `RegExp('/test/(?<test>[a-z]+){0,1}')`
      80             :   /// This will put `{'test': <real>}` to [pathParameters]. Note that we use the name from the regex group.
      81             :   ///
      82             :   /// Optional path segments can be used as a mean to pass data regardless of
      83             :   /// whether there is a browser.
      84             :   ///
      85             :   /// For example: '/books/:id' or using regex `RegExp('/test/(?<test>[a-z]+){0,1}')`
      86             :   List<Pattern> get pathBlueprints;
      87             : 
      88             :   /// Creates and returns the list of pages to be built by the [Navigator]
      89             :   /// when this [BeamLocation] is beamed to or internally inferred.
      90             :   ///
      91             :   /// [context] can be useful while building the pages.
      92             :   /// It will also contain anything injected via [builder].
      93             :   List<BeamPage> buildPages(BuildContext context, T state);
      94             : 
      95             :   /// Guards that will be executing [check] when this gets beamed to.
      96             :   ///
      97             :   /// Checks will be executed in order; chain of responsibility pattern.
      98             :   /// When some guard returns `false`, a candidate will not be accepted
      99             :   /// and stack of pages will be updated as is configured in [BeamGuard].
     100             :   ///
     101             :   /// Override this in your subclasses, if needed.
     102             :   /// See [BeamGuard].
     103           6 :   List<BeamGuard> get guards => const <BeamGuard>[];
     104             : 
     105             :   /// A transition delegate to be used by [Navigator].
     106             :   ///
     107             :   /// This will be used only by this location, unlike
     108             :   /// [BeamerDelegate.transitionDelegate] that will be used for all locations.
     109             :   ///
     110             :   /// This transition delegate will override the one in [BeamerDelegate].
     111             :   ///
     112             :   /// See [Navigator.transitionDelegate].
     113           6 :   TransitionDelegate? get transitionDelegate => null;
     114             : }
     115             : 
     116             : /// Default location to choose if requested URI doesn't parse to any location.
     117             : class NotFound extends BeamLocation<BeamState> {
     118          18 :   NotFound({String path = '/'}) : super(RouteInformation(location: path));
     119             : 
     120           1 :   @override
     121           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     122             : 
     123           6 :   @override
     124           6 :   List<String> get pathBlueprints => [];
     125             : }
     126             : 
     127             : /// Empty location used to intialize a non-nullable BeamLocation variable.
     128             : ///
     129             : /// See [BeamerDelegate.currentBeamLocation].
     130             : class EmptyBeamLocation extends BeamLocation<BeamState> {
     131           1 :   @override
     132           1 :   List<BeamPage> buildPages(BuildContext context, BeamState state) => [];
     133             : 
     134           7 :   @override
     135           7 :   List<String> get pathBlueprints => [];
     136             : }
     137             : 
     138             : /// A beam location for [SimpleLocationBuilder], but can be used freely.
     139             : ///
     140             : /// Useful when needing a simple beam location with a single or few pages.
     141             : class SimpleBeamLocation extends BeamLocation<BeamState> {
     142           6 :   SimpleBeamLocation({
     143             :     required RouteInformation routeInformation,
     144             :     required this.routes,
     145             :     this.navBuilder,
     146           6 :   }) : super(routeInformation);
     147             : 
     148             :   /// Map of all routes this location handles.
     149             :   Map<Pattern, dynamic Function(BuildContext, BeamState)> routes;
     150             : 
     151             :   /// A wrapper used as [BeamLocation.builder].
     152             :   Widget Function(BuildContext context, Widget navigator)? navBuilder;
     153             : 
     154           6 :   @override
     155             :   Widget builder(BuildContext context, Widget navigator) {
     156           6 :     return navBuilder?.call(context, navigator) ?? navigator;
     157             :   }
     158             : 
     159           5 :   int _compareKeys(dynamic a, dynamic b) {
     160             :     // try-catch a CastError
     161             :     try {
     162          15 :       return (a as String).length - (b as String).length;
     163           1 :     } on TypeError {
     164             :       return 1;
     165             :     }
     166             :   }
     167             : 
     168           6 :   @override
     169          18 :   List<Pattern> get pathBlueprints => routes.keys.toList();
     170             : 
     171           6 :   @override
     172             :   List<BeamPage> buildPages(BuildContext context, BeamState state) {
     173          24 :     final filteredRoutes = chooseRoutes(state.routeInformation, routes.keys);
     174          12 :     final activeRoutes = Map.of(routes)
     175          18 :       ..removeWhere((key, value) => !filteredRoutes.containsKey(key));
     176          12 :     final sortedRoutes = activeRoutes.keys.toList()
     177          16 :       ..sort((a, b) => _compareKeys(a, b));
     178          12 :     return sortedRoutes.map<BeamPage>((route) {
     179          12 :       final routeElement = routes[route]!(context, state);
     180           6 :       if (routeElement is BeamPage) {
     181             :         return routeElement;
     182             :       } else {
     183           6 :         return BeamPage(
     184          12 :           key: ValueKey(filteredRoutes[route]),
     185             :           child: routeElement,
     186             :         );
     187             :       }
     188           6 :     }).toList();
     189             :   }
     190             : 
     191             :   /// Chooses all the routes that "sub-match" [state.uri] to stack their pages.
     192             :   ///
     193             :   /// If none of the routes _matches_ [state.uri], nothing will be selected
     194             :   /// and [BeamerDelegate] will declare that the location is [NotFound].
     195           6 :   static Map<Pattern, String> chooseRoutes(
     196             :       RouteInformation routeInformation, Iterable<Pattern> routes) {
     197           6 :     final matched = <Pattern, String>{};
     198             :     bool overrideNotFound = false;
     199          12 :     final uri = Uri.parse(routeInformation.location ?? '/');
     200          12 :     for (final route in routes) {
     201           6 :       if (route is String) {
     202          12 :         final uriPathSegments = uri.pathSegments.toList();
     203          12 :         final routePathSegments = Uri.parse(route).pathSegments;
     204             : 
     205          18 :         if (uriPathSegments.length < routePathSegments.length) {
     206             :           continue;
     207             :         }
     208             : 
     209             :         var checksPassed = true;
     210             :         var path = '';
     211          18 :         for (int i = 0; i < routePathSegments.length; i++) {
     212          18 :           path += '/${uriPathSegments[i]}';
     213             : 
     214          12 :           if (routePathSegments[i] == '*') {
     215             :             overrideNotFound = true;
     216             :             continue;
     217             :           }
     218          12 :           if (routePathSegments[i].startsWith(':')) {
     219             :             continue;
     220             :           }
     221          18 :           if (routePathSegments[i] != uriPathSegments[i]) {
     222             :             checksPassed = false;
     223             :             break;
     224             :           }
     225             :         }
     226             : 
     227             :         if (checksPassed) {
     228          12 :           matched[route] = Uri(
     229           6 :             path: path == '' ? '/' : path,
     230             :             queryParameters:
     231          13 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     232           6 :           ).toString();
     233             :         }
     234             :       } else {
     235           1 :         final regexp = Utils.tryCastToRegExp(route);
     236           2 :         if (regexp.hasMatch(uri.toString())) {
     237           1 :           final path = uri.toString();
     238           2 :           matched[regexp] = Uri(
     239           1 :             path: path == '' ? '/' : path,
     240             :             queryParameters:
     241           2 :                 uri.queryParameters.isEmpty ? null : uri.queryParameters,
     242           1 :           ).toString();
     243             :         }
     244             :       }
     245             :     }
     246             : 
     247             :     bool isNotFound = true;
     248          12 :     matched.forEach((key, value) {
     249           6 :       if (Utils.urisMatch(key, uri)) {
     250             :         isNotFound = false;
     251             :       }
     252             :     });
     253             : 
     254             :     if (overrideNotFound) {
     255             :       return matched;
     256             :     }
     257             : 
     258           3 :     return isNotFound ? {} : matched;
     259             :   }
     260             : }

Generated by: LCOV version 1.14