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

Generated by: LCOV version 1.14