LCOV - code coverage report
Current view: top level - lib - routemaster.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 192 193 99.5 %
Date: 2021-04-04 17:06:32 Functions: 0 0 -

          Line data    Source code
       1             : library routemaster;
       2             : 
       3             : export 'src/parser.dart';
       4             : export 'src/route_info.dart';
       5             : export 'src/pages/guard.dart';
       6             : 
       7             : import 'package:flutter/cupertino.dart';
       8             : import 'package:flutter/foundation.dart';
       9             : import 'package:flutter/material.dart';
      10             : import 'package:flutter/widgets.dart';
      11             : import 'package:path/path.dart';
      12             : import 'package:collection/collection.dart';
      13             : import 'src/pages/guard.dart';
      14             : import 'src/route_dart.dart';
      15             : import 'src/system_nav.dart';
      16             : import 'src/trie_router/trie_router.dart';
      17             : import 'src/route_info.dart';
      18             : 
      19             : part 'src/pages/stack.dart';
      20             : part 'src/pages/tab_pages.dart';
      21             : part 'src/pages/standard.dart';
      22             : 
      23             : typedef RoutemasterBuilder = Widget Function(
      24             :   BuildContext context,
      25             :   List<Page> pages,
      26             :   PopPageCallback onPopPage,
      27             :   GlobalKey<NavigatorState> navigatorKey,
      28             : );
      29             : 
      30             : typedef PageBuilder = Page Function(RouteInfo info);
      31             : 
      32             : typedef UnknownRouteCallback = Page Function(
      33             :   String route,
      34             :   BuildContext context,
      35             : );
      36             : 
      37             : class DefaultUnknownRoutePage extends StatelessWidget {
      38             :   final String route;
      39             : 
      40           2 :   DefaultUnknownRoutePage({required this.route});
      41             : 
      42           2 :   @override
      43             :   Widget build(BuildContext context) {
      44           2 :     return Scaffold(
      45           6 :       body: Text("Page '$route' wasn't found."),
      46             :     );
      47             :   }
      48             : }
      49             : 
      50             : /// An abstract class that can provide a map of routes
      51             : abstract class RouteConfig {
      52             :   /// Called when there's no match for a route. By default this returns
      53             :   /// [DefaultUnknownRoutePage], a simple page not found page.
      54             :   ///
      55             :   /// There are two general options for this callback's operation:
      56             :   ///
      57             :   ///   1. Return a page, which will be displayed.
      58             :   ///
      59             :   /// or
      60             :   ///
      61             :   ///   2. Use the routing delegate to, for instance, redirect to another route
      62             :   ///      and return null.
      63             :   ///
      64           2 :   Page onUnknownRoute(String route, BuildContext context) {
      65           2 :     return MaterialPage<void>(
      66           2 :       child: DefaultUnknownRoutePage(route: route),
      67             :     );
      68             :   }
      69             : 
      70             :   /// Generate a single [RouteResult] for the given [path]. Returns null if the
      71             :   /// path isn't valid.
      72             :   RouterResult? get(String path);
      73             : 
      74             :   /// Generate all [RouteResult] objects required to build the navigation tree
      75             :   /// for the given [path]. Returns null if the path isn't valid.
      76             :   List<RouterResult>? getAll(String path);
      77             : }
      78             : 
      79             : @immutable
      80             : abstract class DefaultRouterConfig extends RouteConfig {
      81             :   final _router = TrieRouter();
      82             : 
      83           9 :   DefaultRouterConfig() {
      84          27 :     _router.addAll(routes);
      85             :   }
      86             : 
      87           6 :   @override
      88          12 :   RouterResult? get(String route) => _router.get(route);
      89             : 
      90           9 :   @override
      91          18 :   List<RouterResult>? getAll(String route) => _router.getAll(route);
      92             : 
      93             :   Map<String, PageBuilder> get routes;
      94             : }
      95             : 
      96             : /// A standard simple routing table which takes a map of routes.
      97             : class RouteMap extends DefaultRouterConfig {
      98             :   /// A map of paths and [PageBuilder] delegates that return [Page] objects to
      99             :   /// build.
     100             :   @override
     101             :   final Map<String, PageBuilder> routes;
     102             : 
     103             :   final UnknownRouteCallback? _onUnknownRoute;
     104             : 
     105           9 :   RouteMap({
     106             :     required this.routes,
     107             :     UnknownRouteCallback? onUnknownRoute,
     108             :   }) : _onUnknownRoute = onUnknownRoute;
     109             : 
     110           3 :   @override
     111             :   Page onUnknownRoute(String route, BuildContext context) {
     112           3 :     if (_onUnknownRoute != null) {
     113           4 :       return _onUnknownRoute!(route, context);
     114             :     }
     115             : 
     116           2 :     return super.onUnknownRoute(route, context);
     117             :   }
     118             : }
     119             : 
     120             : class Routemaster extends RouterDelegate<RouteData> with ChangeNotifier {
     121             :   /// Used to override how the [Navigator] builds.
     122             :   final RoutemasterBuilder? builder;
     123             :   final TransitionDelegate? transitionDelegate;
     124             :   final RouteConfig Function(BuildContext context) routesBuilder;
     125             : 
     126             :   _RoutemasterState _state = _RoutemasterState();
     127             :   bool _isBuilding = false;
     128             : 
     129           9 :   Routemaster({
     130             :     required this.routesBuilder,
     131             :     this.builder,
     132             :     this.transitionDelegate,
     133             :   });
     134             : 
     135           2 :   static Routemaster of(BuildContext context) {
     136             :     return context
     137           2 :         .dependOnInheritedWidgetOfExactType<_RoutemasterWidget>()!
     138           2 :         .delegate;
     139             :   }
     140             : 
     141             :   /// Called by the [Router] when the [Router.backButtonDispatcher] reports that
     142             :   /// the operating system is requesting that the current route be popped.
     143           2 :   @override
     144             :   Future<bool> popRoute() {
     145           4 :     if (_state.stack == null) {
     146           1 :       return SynchronousFuture(false);
     147             :     }
     148             : 
     149           6 :     return _state.stack!.maybePop();
     150             :   }
     151             : 
     152             :   /// Passed to top-level [Navigator] widget, called when the navigator requests
     153             :   /// that it wants to pop a page.
     154           1 :   bool onPopPage(Route<dynamic> route, dynamic result) {
     155           3 :     return _state.stack!.onPopPage(route, result);
     156             :   }
     157             : 
     158             :   /// Replaces the current route with [path].
     159           1 :   void replace(String path, {Map<String, String>? queryParameters}) {
     160             :     if (kIsWeb) {
     161           0 :       SystemNav.replaceLocation(path, queryParameters);
     162             :     } else {
     163           1 :       push(path, queryParameters: queryParameters);
     164             :     }
     165             :   }
     166             : 
     167             :   /// Pushes [path] into the navigation tree.
     168           6 :   void push(String path, {Map<String, String>? queryParameters}) {
     169           6 :     final getAbsolutePath = _getAbsolutePath(path, queryParameters);
     170             : 
     171             :     // Schedule request for next build. This makes sure the routing table is
     172             :     // updated before processing the new path.
     173          12 :     _state.pendingNavigation = getAbsolutePath;
     174           6 :     _markNeedsUpdate();
     175             :   }
     176             : 
     177           6 :   String _getAbsolutePath(String path, Map<String, String>? queryParameters) {
     178             :     final absolutePath =
     179          15 :         isAbsolute(path) ? path : join(currentConfiguration!.path, path);
     180             : 
     181             :     if (queryParameters == null) {
     182             :       return absolutePath;
     183             :     }
     184             : 
     185           2 :     return Uri(path: absolutePath, queryParameters: queryParameters).toString();
     186             :   }
     187             : 
     188             :   /// Generates all pages and sub-pages.
     189           9 :   List<Page> createPages(BuildContext context) {
     190          18 :     assert(_state.stack != null,
     191             :         'Stack must have been created when createPages() is called');
     192          27 :     final pages = _state.stack!.createPages();
     193           9 :     assert(pages.isNotEmpty, 'Returned pages list must not be empty');
     194           9 :     _updateCurrentConfiguration();
     195             : 
     196             :     assert(
     197          27 :       pages.none((page) => page is Redirect),
     198             :       'Returned pages list must not have redirect',
     199             :     );
     200             : 
     201             :     return pages;
     202             :   }
     203             : 
     204           9 :   void _markNeedsUpdate() {
     205           9 :     _updateCurrentConfiguration();
     206             : 
     207           9 :     if (!_isBuilding) {
     208           9 :       notifyListeners();
     209             :     }
     210             :   }
     211             : 
     212           9 :   void _processPendingNavigation() {
     213          18 :     if (_state.pendingNavigation != null) {
     214          18 :       _processNavigation(_state.pendingNavigation!);
     215          12 :       _state.pendingNavigation = null;
     216             :     }
     217             :   }
     218             : 
     219           6 :   void _processNavigation(String path) {
     220           6 :     final pages = _createAllPageWrappers(path);
     221          18 :     _state.stack = StackPageState(delegate: this, routes: pages);
     222             :   }
     223             : 
     224           9 :   @override
     225             :   Widget build(BuildContext context) {
     226           9 :     return _DependencyTracker(
     227             :       delegate: this,
     228           9 :       builder: (context) {
     229           9 :         _isBuilding = true;
     230           9 :         _init(context);
     231           9 :         _processPendingNavigation();
     232           9 :         final pages = createPages(context);
     233           9 :         _isBuilding = false;
     234             : 
     235           9 :         return _RoutemasterWidget(
     236             :           delegate: this,
     237           9 :           child: builder != null
     238           6 :               ? builder!(context, pages, onPopPage, _state.stack!.navigatorKey)
     239           9 :               : Navigator(
     240             :                   pages: pages,
     241           9 :                   onPopPage: onPopPage,
     242          27 :                   key: _state.stack!.navigatorKey,
     243           9 :                   transitionDelegate: transitionDelegate ??
     244             :                       const DefaultTransitionDelegate<dynamic>(),
     245             :                 ),
     246             :         );
     247             :       },
     248             :     );
     249             :   }
     250             : 
     251             :   // Returns a [RouteData] that matches the current route state.
     252             :   // This is used to update a browser's current URL.
     253             : 
     254           9 :   @override
     255             :   RouteData? get currentConfiguration {
     256          18 :     return _state.currentConfiguration;
     257             :   }
     258             : 
     259           9 :   void _updateCurrentConfiguration() {
     260          18 :     if (_state.stack == null) {
     261             :       return;
     262             :     }
     263             : 
     264          54 :     final path = _state.stack!._getCurrentPages().last.routeInfo.path;
     265          18 :     print("Updated path: '$path'");
     266          27 :     _state.currentConfiguration = RouteData(path);
     267             :   }
     268             : 
     269             :   // Called when a new URL is set. The RouteInformationParser will parse the
     270             :   // URL, and return a new [RouteData], that gets passed this this method.
     271             :   //
     272             :   // This method then modifies the state based on that information.
     273           1 :   @override
     274             :   Future<void> setNewRoutePath(RouteData routeData) {
     275           2 :     push(routeData.path);
     276           1 :     return SynchronousFuture(null);
     277             :   }
     278             : 
     279           9 :   @override
     280             :   Future<void> setInitialRoutePath(RouteData configuration) {
     281          36 :     _state.currentConfiguration = RouteData(configuration.path);
     282           9 :     return SynchronousFuture(null);
     283             :   }
     284             : 
     285           9 :   void _init(BuildContext context, {bool isRebuild = false}) {
     286          18 :     if (_state.routeConfig == null) {
     287          36 :       _state.routeConfig = routesBuilder(context);
     288             : 
     289             :       final path =
     290          36 :           _state.pendingNavigation ?? currentConfiguration?.path ?? '/';
     291           9 :       final pageStates = _createAllPageWrappers(path);
     292             : 
     293           9 :       assert(pageStates.isNotEmpty);
     294          27 :       _state.stack = StackPageState(delegate: this, routes: pageStates);
     295             :     }
     296             :   }
     297             : 
     298             :   /// Called when dependencies of the [routesBuilder] changed.
     299           9 :   void _didChangeDependencies(BuildContext context) {
     300           9 :     if (currentConfiguration == null) {
     301             :       return;
     302             :     }
     303             : 
     304          36 :     WidgetsBinding.instance?.addPostFrameCallback((_) => _markNeedsUpdate());
     305             : 
     306             :     // Reset state
     307          18 :     _state.routeConfig = null;
     308          18 :     _state.stack = null;
     309             : 
     310           9 :     _isBuilding = true;
     311           9 :     _init(context, isRebuild: true);
     312           9 :     _isBuilding = false;
     313             :   }
     314             : 
     315           3 :   PageWrapper _onUnknownRoute(String requestedPath) {
     316           6 :     print("Router couldn't find a match for path '$requestedPath''");
     317             : 
     318           9 :     final result = _state.routeConfig!.onUnknownRoute(
     319             :       requestedPath,
     320           9 :       _state.globalKey.currentContext!,
     321             :     );
     322             : 
     323           3 :     if (result is Redirect) {
     324           2 :       return _getPageWrapper(
     325           2 :         Uri(
     326           2 :           path: result.path,
     327           2 :           queryParameters: result.queryParameters,
     328           2 :         ).toString(),
     329             :       );
     330             :     }
     331             : 
     332             :     // Return 404 page
     333           3 :     final routeInfo = RouteInfo(requestedPath);
     334           3 :     return StatelessPage(routeInfo: routeInfo, page: result);
     335             :   }
     336             : 
     337           9 :   List<PageWrapper> _createAllPageWrappers(
     338             :     String requestedPath, {
     339             :     List<String>? redirects,
     340             :   }) {
     341          27 :     final routerResult = _state.routeConfig!.getAll(requestedPath);
     342             : 
     343           9 :     if (routerResult == null || routerResult.isEmpty) {
     344           2 :       return [_onUnknownRoute(requestedPath)];
     345             :     }
     346             : 
     347          28 :     final currentRoutes = _state.stack?._getCurrentPages().toList();
     348           9 :     var result = <PageWrapper>[];
     349             :     var i = 0;
     350             : 
     351          18 :     for (final routerData in routerResult.reversed) {
     352           9 :       final isLastRoute = i == 0;
     353           9 :       final routeInfo = RouteInfo.fromRouterResult(
     354             :         routerData,
     355             :         // Only the last route gets query parameters
     356           8 :         isLastRoute ? requestedPath : routerData.pathSegment,
     357             :       );
     358             : 
     359           9 :       final current = _getOrCreatePageWrapper(
     360             :         requestedPath,
     361             :         routeInfo,
     362             :         currentRoutes,
     363             :         routerData,
     364             :       );
     365             : 
     366           9 :       if (current is _RedirectWrapper) {
     367             :         if (isLastRoute) {
     368             :           if (kDebugMode) {
     369             :             if (redirects == null) {
     370           2 :               redirects = [requestedPath];
     371             :             } else {
     372           1 :               if (redirects.contains(requestedPath)) {
     373           1 :                 redirects.add(requestedPath);
     374           1 :                 throw RedirectLoopError(redirects);
     375             :               }
     376           1 :               redirects.add(requestedPath);
     377             :             }
     378             :           }
     379             : 
     380           2 :           return _createAllPageWrappers(
     381           4 :             current.redirectPage.absolutePath,
     382             :             redirects: redirects,
     383             :           );
     384             :         } else {
     385             :           continue;
     386             :         }
     387             :       }
     388             : 
     389          15 :       if (result.isNotEmpty && current.maybeSetChildPages(result)) {
     390           4 :         result = [current];
     391             :       } else {
     392           9 :         result.insert(0, current);
     393             :       }
     394             : 
     395           9 :       i++;
     396             :     }
     397             : 
     398           9 :     assert(result.isNotEmpty, "_createAllStates can't return empty list");
     399             :     return result;
     400             :   }
     401             : 
     402             :   /// If there's a current route matching the path in the tree, return it.
     403             :   /// Otherwise create a new one. This could possibly be made more efficient
     404             :   /// By using a map rather than iterating over all currentRoutes.
     405           9 :   PageWrapper _getOrCreatePageWrapper(
     406             :     String requestedPath,
     407             :     RouteInfo routeInfo,
     408             :     List<PageWrapper>? currentRoutes,
     409             :     RouterResult routerResult,
     410             :   ) {
     411             :     if (currentRoutes != null) {
     412           5 :       final currentState = currentRoutes.firstWhereOrNull(
     413          15 :         ((element) => element.routeInfo == routeInfo),
     414             :       );
     415             : 
     416             :       if (currentState != null) {
     417             :         return currentState;
     418             :       }
     419             :     }
     420             : 
     421           9 :     return _createPageWrapper(
     422             :       requestedPath: requestedPath,
     423          18 :       page: routerResult.builder(routeInfo),
     424             :       routeInfo: routeInfo,
     425             :     );
     426             :   }
     427             : 
     428             :   /// Try to get the route for [requestedPath]. If no match, returns default path.
     429             :   /// Returns null if validation fails.
     430           6 :   PageWrapper _getPageWrapper(String requestedPath) {
     431          18 :     final routerResult = _state.routeConfig!.get(requestedPath);
     432             :     if (routerResult == null) {
     433           1 :       return _onUnknownRoute(requestedPath);
     434             :     }
     435             : 
     436           6 :     final routeInfo = RouteInfo.fromRouterResult(routerResult, requestedPath);
     437          12 :     final page = routerResult.builder(routeInfo);
     438             : 
     439           6 :     if (page is Redirect) {
     440           2 :       return _getPageWrapper(page.path);
     441             :     }
     442             : 
     443           6 :     return _createPageWrapper(
     444             :       requestedPath: requestedPath,
     445             :       page: page,
     446             :       routeInfo: routeInfo,
     447             :     );
     448             :   }
     449             : 
     450           9 :   PageWrapper _createPageWrapper({
     451             :     required String requestedPath,
     452             :     required Page page,
     453             :     required RouteInfo routeInfo,
     454             :   }) {
     455           9 :     while (page is ProxyPage) {
     456           2 :       if (page is GuardedPage) {
     457           6 :         final context = _state.globalKey.currentContext!;
     458           4 :         if (!page.validate(routeInfo, context)) {
     459           3 :           print("Validation failed for '${routeInfo.path}'");
     460             : 
     461           1 :           if (page.onValidationFailed == null) {
     462           1 :             return _onUnknownRoute(requestedPath);
     463             :           }
     464             : 
     465           2 :           final result = page.onValidationFailed!(routeInfo, context);
     466           1 :           return _createPageWrapper(
     467             :             requestedPath: requestedPath,
     468             :             page: result,
     469             :             routeInfo: routeInfo,
     470             :           );
     471             :         }
     472             :       }
     473             : 
     474           2 :       page = page.child;
     475             :     }
     476             : 
     477           9 :     if (page is StatefulPage) {
     478           4 :       return page.createState(this, routeInfo);
     479             :     }
     480             : 
     481           9 :     if (page is Redirect) {
     482           2 :       return _RedirectWrapper(page);
     483             :     }
     484             : 
     485           9 :     assert(page is! Redirect, 'Redirect has not been followed');
     486           9 :     assert(page is! ProxyPage, 'ProxyPage has not been unwrapped');
     487             : 
     488             :     // Page is just a standard Flutter page, create a wrapper for it
     489           9 :     return StatelessPage(routeInfo: routeInfo, page: page);
     490             :   }
     491             : }
     492             : 
     493             : /// Used internally so descendent widgets can use `Routemaster.of(context)`.
     494             : class _RoutemasterWidget extends InheritedWidget {
     495             :   final Routemaster delegate;
     496             : 
     497           9 :   const _RoutemasterWidget({
     498             :     required Widget child,
     499             :     required this.delegate,
     500           9 :   }) : super(child: child);
     501             : 
     502           7 :   @override
     503             :   bool updateShouldNotify(covariant _RoutemasterWidget oldWidget) {
     504          21 :     return delegate != oldWidget.delegate;
     505             :   }
     506             : }
     507             : 
     508             : class _RoutemasterState {
     509             :   final globalKey = GlobalKey(debugLabel: 'routemaster');
     510             :   StackPageState? stack;
     511             :   RouteConfig? routeConfig;
     512             :   RouteData? currentConfiguration;
     513             :   String? pendingNavigation;
     514             : }
     515             : 
     516             : /// Widget to trigger router rebuild when dependencies change
     517             : class _DependencyTracker extends StatefulWidget {
     518             :   final Routemaster delegate;
     519             :   final Widget Function(BuildContext context) builder;
     520             : 
     521           9 :   _DependencyTracker({
     522             :     required this.delegate,
     523             :     required this.builder,
     524          27 :   }) : super(key: delegate._state.globalKey);
     525             : 
     526           9 :   @override
     527           9 :   _DependencyTrackerState createState() => _DependencyTrackerState();
     528             : }
     529             : 
     530             : class _DependencyTrackerState extends State<_DependencyTracker> {
     531             :   late _RoutemasterState _delegateState;
     532             : 
     533           9 :   @override
     534             :   Widget build(BuildContext context) {
     535          27 :     return widget.builder(context);
     536             :   }
     537             : 
     538           9 :   @override
     539             :   void initState() {
     540           9 :     super.initState();
     541          36 :     _delegateState = widget.delegate._state;
     542          36 :     widget.delegate._state = _delegateState;
     543             :   }
     544             : 
     545           7 :   @override
     546             :   void didUpdateWidget(_DependencyTracker oldWidget) {
     547           7 :     super.didUpdateWidget(oldWidget);
     548          28 :     widget.delegate._state = _delegateState;
     549             :   }
     550             : 
     551           9 :   @override
     552             :   void didChangeDependencies() {
     553           9 :     super.didChangeDependencies();
     554          36 :     widget.delegate._didChangeDependencies(this.context);
     555             :   }
     556             : }
     557             : 
     558             : class RedirectLoopError extends Error {
     559             :   final List<String> redirects;
     560             : 
     561           1 :   RedirectLoopError(this.redirects);
     562             : 
     563           1 :   @override
     564             :   String toString() {
     565           1 :     return 'Routemaster is stuck in an endless redirect loop:\n\n' +
     566           1 :         redirects
     567           4 :             .take(redirects.length - 1)
     568           2 :             .mapIndexed((i, path1) =>
     569           4 :                 "  * '$path1' redirected to '${redirects[i + 1]}'")
     570           2 :             .join('\n') +
     571             :         '\n\nThis is an error in your routing map.';
     572             :   }
     573             : }

Generated by: LCOV version 1.15