LCOV - code coverage report
Current view: top level - src - navigation_scheme.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 184 202 91.1 %
Date: 2023-03-02 15:31:08 Functions: 0 0 -

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

Generated by: LCOV version