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

Generated by: LCOV version