LCOV - code coverage report
Current view: top level - src - navigation_scheme.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 125 136 91.9 %
Date: 2022-12-25 21:41:53 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : 
       3             : import 'package:collection/collection.dart';
       4             : import 'package:flutter/foundation.dart';
       5             : import 'package:flutter/widgets.dart';
       6             : 
       7             : import 'destination.dart';
       8             : import 'exceptions.dart';
       9             : import 'navigation_controller.dart';
      10             : import 'router_delegate.dart';
      11             : import 'route_parser.dart';
      12             : import 'utils/utils.dart';
      13             : 
      14             : /// Defines a navigation scheme of the app.
      15             : ///
      16             : /// Contains a list of possible [destinations] to navigate in the app.
      17             : /// Each destination can be a final, i.e. is directly rendered as a some screen,
      18             : /// or might include a nested navigator with its own destinations.
      19             : ///
      20             : /// Until the custom [navigator] is provided, the NavigatorScheme creates a default
      21             : /// root navigator, that manages top level destinations.
      22             : ///
      23             : /// In case of some navigation error, user will be redirected to [errorDestination],
      24             : /// if it is specified. Otherwise an exception will be thrown.
      25             : ///
      26             : /// See also:
      27             : /// - [Destination]
      28             : /// - [NavigationController]
      29             : ///
      30             : class NavigationScheme with ChangeNotifier {
      31             :   /// Creates navigation scheme.
      32             :   ///
      33           5 :   NavigationScheme({
      34             :     List<Destination> destinations = const <Destination>[],
      35             :     this.errorDestination,
      36             :     this.waitingOverlayBuilder,
      37             :     NavigationController? navigator,
      38             :   })  : assert(
      39          12 :             (destinations.isEmpty ? navigator!.destinations : destinations)
      40          15 :                 .any((destination) => destination.isHome),
      41             :             'One of destinations must be a home destination.'),
      42             :         assert(
      43           5 :             errorDestination == null ||
      44             :                 navigator == null ||
      45           2 :                 (navigator.destinations
      46           6 :                     .any((destination) => destination == errorDestination)),
      47             :             'When "errorDestination" and custom "navigator" are specified, you must include the "errorDestination" to the "navigator"s destinations') {
      48           5 :     _rootNavigator = navigator ??
      49           5 :         NavigationController(
      50           5 :           destinations: <Destination>[
      51             :             ...destinations,
      52          15 :             if (errorDestination != null) errorDestination!,
      53             :           ],
      54             :           tag: 'Root',
      55             :         );
      56          10 :     _routerDelegate = TheseusRouterDelegate(navigationScheme: this);
      57          10 :     _routeParser = TheseusRouteInformationParser(navigationScheme: this);
      58          15 :     _currentDestination = _rootNavigator.currentDestination;
      59          10 :     _initializeNavigator(_rootNavigator);
      60           5 :     _updateCurrentDestination(backFrom: null);
      61             :   }
      62             : 
      63             :   /// The destination to redirect in case of error.
      64             :   ///
      65             :   final Destination? errorDestination;
      66             : 
      67             :   /// Returns a widget to display while destination is resolving.
      68             :   ///
      69             :   /// Resolving the destination might be asynchronous, for example, because of parsing typed
      70             :   /// parameters or checking redirection conditions.
      71             :   ///
      72             :   /// In these cases this function is used to build a widget, which would be displayed
      73             :   /// until the current destination is resolved.
      74             :   ///
      75             :   final Widget Function(BuildContext context, Destination destination)?
      76             :       waitingOverlayBuilder;
      77             : 
      78             :   late Destination _currentDestination;
      79             : 
      80             :   /// The current destination within the whole navigation scheme.
      81             :   ///
      82             :   /// To get a current top level destination, use [rootNavigator.currentDestination].
      83             :   ///
      84           8 :   Destination get currentDestination => _currentDestination;
      85             : 
      86             :   bool _isResolving = false;
      87             : 
      88             :   /// Indicates if a current destination is in resolving state.
      89             :   ///
      90             :   /// This flag is turned on during performing of the redirection validations, or
      91             :   /// parsing of typed parameters.
      92             :   ///
      93           4 :   bool get isResolving => _isResolving;
      94             : 
      95             :   final _navigatorListeners = <NavigationController, VoidCallback?>{};
      96             : 
      97             :   final _navigatorMatches = <Destination, NavigationController>{};
      98             : 
      99             :   final _navigatorOwners = <NavigationController, Destination>{};
     100             : 
     101             :   final _destinationCompleters = <Destination, Completer<void>>{};
     102             : 
     103             :   late final NavigationController _rootNavigator;
     104             : 
     105             :   /// The root navigator in the navigation scheme.
     106             :   ///
     107             :   /// This navigator manages top level destinations.
     108             :   ///
     109           6 :   NavigationController get rootNavigator => _rootNavigator;
     110             : 
     111             :   late final TheseusRouterDelegate _routerDelegate;
     112             : 
     113             :   /// Reference to the RouterDelegate implementation
     114             :   ///
     115           6 :   TheseusRouterDelegate get routerDelegate => _routerDelegate;
     116             : 
     117             :   late final TheseusRouteInformationParser _routeParser;
     118             : 
     119             :   /// Reference to the RouteInformationParser implementation
     120             :   ///
     121           4 :   TheseusRouteInformationParser get routeParser => _routeParser;
     122             : 
     123             :   /// Stores the original destination in case of redirection.
     124             :   ///
     125           0 :   Destination? get redirectedFrom =>
     126           0 :       _currentDestination.settings.redirectedFrom;
     127             : 
     128             :   bool _shouldClose = false;
     129             : 
     130             :   /// Whether the app should close.
     131             :   ///
     132             :   /// This flag is set on when user perform 'Back' action on top most destination.
     133             :   ///
     134           8 :   bool get shouldClose => _shouldClose;
     135             : 
     136           2 :   @override
     137             :   void dispose() {
     138           4 :     _routerDelegate.dispose();
     139           2 :     _removeNavigatorListeners();
     140           2 :     super.dispose();
     141             :   }
     142             : 
     143             :   /// Find a destination in the scheme that match a given URI.
     144             :   ///
     145             :   /// Returns 'null' if no destination matching the URI was found.
     146             :   ///
     147          15 :   Destination? findDestination(String uri) => _navigatorMatches.keys
     148          15 :       .firstWhereOrNull((destination) => destination.isMatch(uri));
     149             : 
     150             :   /// Returns a proper navigator in the navigation scheme for a given destination.
     151             :   ///
     152             :   /// Returns 'null' if no navigator found.
     153             :   ///
     154           4 :   NavigationController? findNavigator(Destination destination) =>
     155          16 :       _navigatorMatches[findDestination(destination.path)];
     156             : 
     157             :   /// Opens the specified [destination].
     158             :   ///
     159             :   /// First, searches the navigation scheme for proper navigator for the destination.
     160             :   /// If found, uses the navigator's [goTo] method to open the destination.
     161             :   /// Otherwise throws [UnknownDestinationException].
     162             :   ///
     163           4 :   Future<void> goTo(Destination destination) {
     164           4 :     final navigator = findNavigator(destination);
     165             :     if (navigator == null) {
     166           2 :       _handleError(destination);
     167           2 :       return SynchronousFuture(null);
     168             :     }
     169           8 :     Log.d(runtimeType,
     170          16 :         'goTo(): navigator=${navigator.tag}, destination=$destination, redirectedFrom=${destination.settings.redirectedFrom}');
     171           4 :     _shouldClose = false;
     172             : 
     173           4 :     final completer = Completer<void>();
     174             :     var destinationToComplete = destination;
     175           8 :     _destinationCompleters[destinationToComplete] = completer;
     176           4 :     while (!destinationToComplete.isFinalDestination) {
     177             :       destinationToComplete =
     178           6 :           destinationToComplete.navigator!.currentDestination;
     179           6 :       _destinationCompleters[destinationToComplete] = completer;
     180             :     }
     181           4 :     navigator.goTo(destination);
     182             : 
     183           4 :     return completer.future;
     184             :   }
     185             : 
     186             :   /// Close the current destination.
     187             :   ///
     188             :   /// If the current destination is the last one, this requests closing the app by
     189             :   /// setting the [shouldClose] flag.
     190             :   ///
     191           3 :   void goBack() {
     192           6 :     final navigator = findNavigator(_currentDestination);
     193             :     if (navigator == null) {
     194           0 :       _handleError(_currentDestination);
     195             :       return;
     196             :     }
     197           3 :     navigator.goBack();
     198             :   }
     199             : 
     200             :   /// Resolves the current destination
     201             :   ///
     202             :   /// Applies redirection validations to the current destination.
     203             :   /// While validations are performed, the [isResolving] flag is set to true.
     204             :   /// This allows to display a widget returned by [waitingOverlayBuilder]
     205             :   /// until the destination is resolved.
     206             :   ///
     207             :   /// In case of validation are not passed, redirects to corresponding redirection destination.
     208             :   ///
     209           4 :   Future<void> resolve() async {
     210           4 :     Timer isResolvingTimer = Timer(
     211             :       const Duration(milliseconds: 500),
     212           2 :       () {
     213           2 :         if (!_isResolving) {
     214           2 :           _isResolving = true;
     215           2 :           notifyListeners();
     216             :         }
     217             :       },
     218             :     );
     219           4 :     final requestedDestination = _currentDestination;
     220           8 :     final resolvedDestination = await _resolveDestination(requestedDestination);
     221           4 :     isResolvingTimer.cancel();
     222           8 :     Log.d(runtimeType,
     223           4 :         'resolve(): requestedDestination=$requestedDestination, resolvedDestination=$resolvedDestination');
     224           8 :     if (requestedDestination != _currentDestination) {
     225           2 :       _isResolving = false;
     226           2 :       notifyListeners();
     227             :       return;
     228             :     }
     229           4 :     if (resolvedDestination == requestedDestination) {
     230           4 :       _isResolving = false;
     231           4 :       _completeResolvedDestination(requestedDestination);
     232           4 :       notifyListeners();
     233             :       return;
     234             :     }
     235           6 :     goTo(resolvedDestination.withSettings(resolvedDestination.settings
     236           2 :         .copyWith(redirectedFrom: requestedDestination)));
     237             :   }
     238             : 
     239           5 :   void _initializeNavigator(NavigationController navigator) {
     240           8 :     void listener() => _onNavigatorStateChanged(navigator);
     241             : 
     242             :     // Add a listener of the navigator
     243          10 :     _navigatorListeners[navigator] = listener;
     244           5 :     navigator.addListener(listener);
     245             : 
     246          10 :     for (var destination in navigator.destinations) {
     247          10 :       _navigatorMatches[destination] = navigator;
     248           5 :       if (!destination.isFinalDestination) {
     249             :         // Set navigation owner
     250          12 :         _navigatorOwners[destination.navigator!] = destination;
     251             :         // Initialize nested navigator
     252           8 :         _initializeNavigator(destination.navigator!);
     253             :       }
     254             :     }
     255             :   }
     256             : 
     257           2 :   void _handleError(Destination? destination) {
     258           2 :     if (errorDestination != null) {
     259          10 :       goTo((errorDestination!).withSettings(errorDestination!.settings
     260           2 :           .copyWith(redirectedFrom: destination)));
     261             :     } else {
     262           2 :       throw UnknownDestinationException(destination);
     263             :     }
     264             :   }
     265             : 
     266           2 :   void _removeNavigatorListeners() {
     267           6 :     for (var navigator in _navigatorListeners.keys) {
     268           6 :       navigator.removeListener(_navigatorListeners[navigator]!);
     269             :     }
     270             :   }
     271             : 
     272           4 :   void _onNavigatorStateChanged(NavigationController navigator) {
     273           8 :     Log.d(runtimeType,
     274          20 :         'onNavigatorStateChanged(): navigator=${navigator.tag}, error=${navigator.error}, backFrom=${navigator.backFrom}, shouldClose=${navigator.shouldClose}');
     275           4 :     if (navigator.hasError) {
     276           0 :       _handleError(navigator.error!.destination);
     277             :     }
     278           8 :     final owner = _navigatorOwners[navigator];
     279             :     if (owner != null) {
     280           6 :       Log.d(runtimeType, 'onNavigatorStateChanged(): owner=$owner');
     281           2 :       if (navigator.backFrom != null) {
     282           0 :         if (navigator.shouldClose) {
     283           0 :           final parentNavigator = findNavigator(owner);
     284             :           if (parentNavigator == null) {
     285           0 :             _handleError(owner);
     286             :             return;
     287             :           }
     288           0 :           parentNavigator.goBack();
     289             :         } else {
     290           0 :           _updateCurrentDestination(backFrom: navigator.backFrom);
     291             :         }
     292             :       } else {
     293           6 :         if (navigator.currentDestination.settings.reset) {
     294           2 :           goTo(owner
     295           6 :               .withSettings(owner.settings.copyWith(reset: true)));
     296             :         } else {
     297           0 :           goTo(owner);
     298             :         }
     299             :       }
     300             :     } else {
     301           8 :       _updateCurrentDestination(backFrom: navigator.backFrom);
     302             :     }
     303             :   }
     304             : 
     305           5 :   void _updateCurrentDestination({required Destination? backFrom}) {
     306             :     // TODO: Do we need the stack here?
     307          15 :     List<Destination> newStack = List.from(_rootNavigator.stack);
     308             :     // TODO: Probably '_shouldClose' variable is not needed, we can use '_rootNavigator' directly
     309          15 :     _shouldClose = _rootNavigator.shouldClose;
     310           5 :     if (_shouldClose) {
     311           6 :       Log.d(runtimeType,
     312           9 :           'updateCurrentDestination(): currentDestination=$_currentDestination, shouldClose=$_shouldClose');
     313           3 :       notifyListeners();
     314             :       return;
     315             :     }
     316             : 
     317          10 :     Destination newDestination = _rootNavigator.currentDestination;
     318           5 :     while (!newDestination.isFinalDestination) {
     319           9 :       newStack.addAll(newDestination.navigator!.stack);
     320           6 :       newDestination = newDestination.navigator!.currentDestination;
     321             :     }
     322          10 :     Log.d(runtimeType,
     323          10 :         'updateCurrentDestination(): currentDestination=$_currentDestination, newDestination=$newDestination');
     324          10 :     if (_currentDestination != newDestination ||
     325          10 :         newDestination.settings.reset) {
     326           4 :       _currentDestination = newDestination;
     327          14 :       if (_currentDestination == backFrom?.settings.redirectedFrom) {
     328           2 :         notifyListeners();
     329             :         return;
     330             :       }
     331           4 :       resolve();
     332             :     }
     333             :   }
     334             : 
     335           4 :   Future<Destination> _resolveDestination(Destination destination) async {
     336             :     // Check redirections that are defined for given destination
     337           7 :     for (var redirection in destination.redirections) {
     338           6 :       if (!(await redirection.validate(destination))) {
     339           6 :         return await _resolveDestination(redirection.destination);
     340             :       }
     341             :     }
     342             :     // In case of nested destination, validate the owner
     343           4 :     final navigator = findNavigator(destination);
     344             :     if (navigator == null) {
     345           0 :       throw UnknownDestinationException(destination);
     346             :     }
     347           8 :     final owner = _navigatorOwners[navigator];
     348             :     if (owner == null) {
     349             :       return destination;
     350             :     }
     351           6 :     final resolvedOwner = await _resolveDestination(owner);
     352           3 :     return owner != resolvedOwner ? resolvedOwner : destination;
     353             :   }
     354             : 
     355           4 :   void _completeResolvedDestination(Destination destination) {
     356             :     Destination? destinationToComplete = destination;
     357             :     while (destinationToComplete != null) {
     358          12 :       if (!(_destinationCompleters[destinationToComplete]?.isCompleted ??
     359             :           true)) {
     360          12 :         _destinationCompleters[destinationToComplete]?.complete();
     361             :       }
     362             :       destinationToComplete =
     363           8 :           destinationToComplete.settings.redirectedFrom;
     364             :     }
     365             :   }
     366             : }

Generated by: LCOV version