LCOV - code coverage report
Current view: top level - src - navigation_controller.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 58 62 93.5 %
Date: 2022-10-22 16:29:45 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:collection';
       2             : import 'package:flutter/widgets.dart';
       3             : 
       4             : import 'destination.dart';
       5             : import 'exceptions.dart';
       6             : import 'utils/utils.dart';
       7             : import 'widgets/index.dart';
       8             : 
       9             : /// A [NavigationController] manages the navigation state.
      10             : ///
      11             : /// Using the given [destinations] list, it maintains the navigation [stack].
      12             : ///
      13             : /// The navigation stack is updated when a user navigates to specified destination
      14             : /// by calling [goTo] method, or returns back with [goBack] method.
      15             : ///
      16             : /// The navigation controller (navigator) provides an access to a [currentDestination],
      17             : /// which is one on the top of the stack.
      18             : ///
      19             : /// Initially, the navigation stack contains a destination at [initialDestinationIndex]
      20             : /// in the provided list of destinations.
      21             : ///
      22             : /// [NavigationController] implements [ChangeNotifier] and notifies its listener when
      23             : /// the [currentDestination]/[stack] is changed, or some error was happened.
      24             : ///
      25             : /// See also:
      26             : /// - [Destination]
      27             : /// - [NavigationScheme]
      28             : /// - [NavigationControllerError]
      29             : ///
      30             : class NavigationController with ChangeNotifier {
      31             :   /// Creates navigation controller instance.
      32             :   ///
      33             :   /// Add initial destination to the navigation stack and creates a [GlobalKey] for
      34             :   /// a [Navigator] widget.
      35             :   ///
      36           6 :   NavigationController({
      37             :     required this.destinations,
      38             :     this.builder = const DefaultNavigatorBuilder(),
      39             :     this.initialDestinationIndex = 0,
      40             :     this.notifyOnError = true,
      41             :     this.tag = '',
      42             :   }) {
      43          30 :     _stack.add(destinations[initialDestinationIndex]);
      44          18 :     key = GlobalKey<NavigatorState>(debugLabel: tag);
      45             :   }
      46             : 
      47             :   /// List of destinations, which this navigator operate of.
      48             :   ///
      49             :   final List<Destination> destinations;
      50             : 
      51             :   /// An implementation of [NavigatorBuilder] that creates a wrapping widget tree
      52             :   /// around destinations.
      53             :   ///
      54             :   /// Defaults to [DefaultNavigatorBuilder] that wraps destinations to Flutter's
      55             :   /// [Navigator] widget.
      56             :   ///
      57             :   /// Also the following implementations are available:
      58             :   /// - [BottomNavigationBuilder] allows to switch destination using Flutter's
      59             :   /// [BottomNavigationBar] widget.
      60             :   /// - [DrawerNavigationBuilder] uses drawer menu to navigate top-level destinations.
      61             :   /// - [TabsNavigationBuilder] uses [TabBar] with [TabBarView] widgets to switch destinations.
      62             :   ///
      63             :   /// You can implement your custom wrapper by extending the [NavigatorBuilder] class.
      64             :   ///
      65             :   /// See also:
      66             :   /// - [NavigatorBuilder]
      67             :   /// - [DefaultNavigatorBuilder]
      68             :   /// - [BottomNavigationBuilder]
      69             :   /// - [DrawerNavigationBuilder]
      70             :   /// - [TabsNavigationBuilder]
      71             :   ///
      72             :   final NavigatorBuilder builder;
      73             : 
      74             :   /// Index of the initial destination.
      75             :   ///
      76             :   /// Initial destination will be added to the navigation [stack] on creation the
      77             :   /// navigator. If it is omitted, the first destination in the [destinations] list
      78             :   /// will be used as initial one.
      79             :   ///
      80             :   final int initialDestinationIndex;
      81             : 
      82             :   /// Whether to notify listeners on errors in navigation actions.
      83             :   ///
      84             :   /// Defaults to true. Basically the [NavigationScheme] handles the errors.
      85             :   /// When set to false, the exception will be thrown on errors instead of notifying listeners.
      86             :   ///
      87             :   final bool notifyOnError;
      88             : 
      89             :   /// An identifier of this navigator.
      90             :   ///
      91             :   /// It is used in the debug logs to identify entries related to this navigator.
      92             :   ///
      93             :   final String? tag;
      94             : 
      95             :   /// Provides the global key for corresponding [Navigator] widget.
      96             :   ///
      97             :   late final GlobalKey<NavigatorState> key;
      98             : 
      99             :   NavigationControllerError? _error;
     100             : 
     101             :   /// Error details
     102             :   ///
     103          10 :   NavigationControllerError? get error => _error;
     104             : 
     105             :   /// Whether an error was happened on [goTo()] or [goBack()] actions.
     106             :   ///
     107           8 :   bool get hasError => _error != null;
     108             : 
     109             :   bool _gotBack = false;
     110             : 
     111             :   /// Whether the back action was performed.
     112             :   ///
     113             :   /// It is 'true' if the last navigation method call was the [goBack()].
     114             :   ///
     115          10 :   bool get gotBack => _gotBack;
     116             : 
     117             :   bool _shouldClose = false;
     118             : 
     119             :   /// Whether the navigator should close.
     120             :   ///
     121             :   /// It is set to 'true' when user call [onBack] method when the only destination
     122             :   /// is in the stack.
     123             :   /// If this is the root navigator in the [NavigationScheme], setting [shouldClose]
     124             :   /// to true will cause closing the app.
     125             :   ///
     126          12 :   bool get shouldClose => _shouldClose;
     127             : 
     128             :   final _stack = Queue<Destination>();
     129             : 
     130          20 :   String get _tag => '$runtimeType::$tag';
     131             : 
     132             :   /// The current destination of the navigator.
     133             :   ///
     134             :   /// It is the top destination in the navigation [stack].
     135             :   ///
     136          18 :   Destination get currentDestination => _stack.last;
     137             : 
     138             :   /// The navigation [stack].
     139             :   ///
     140             :   /// When [goTo] method is called, the destination is added to the stack,
     141             :   /// and when [goBack] method is called, the [currentDestination] is removed from
     142             :   /// the stack.
     143             :   ///
     144          18 :   List<Destination> get stack => _stack.toList();
     145             : 
     146             :   /// Builds a widget that wraps the destination's content.
     147             :   ///
     148           2 :   Widget build(BuildContext context) {
     149           4 :     return builder.build(context, this);
     150             :   }
     151             : 
     152             :   /// Opens specified destination.
     153             :   ///
     154             :   /// By calling calling this method, depending on [destination.configuration],
     155             :   /// the given destination will be either added to the navigation [stack], or
     156             :   /// will replace the current destination.
     157             :   ///
     158             :   /// Also, missing upward destinations can be added to the stack, if the
     159             :   /// current stack state doesn't match, and the [destination.upwardDestinationBuilder]
     160             :   /// is defined. This mostly could happen when it is navigated as a deeplink.
     161             :   ///
     162             :   /// Throws [UnknownDestinationException] if the navigator's [destinations]
     163             :   /// doesn't contain given destination.
     164             :   ///
     165           5 :   Future<void> goTo(Destination destination) async {
     166          10 :     Log.d(_tag,
     167          15 :         'goTo(): destination=$destination, reset=${destination.configuration.reset}');
     168           5 :     _error = null;
     169           5 :     _gotBack = false;
     170           5 :     _shouldClose = false;
     171          10 :     if (currentDestination == destination) {
     172           8 :       if (!destination.configuration.reset) {
     173           8 :         Log.d(_tag,
     174             :             'goTo(): The destination is already on top. No action required.');
     175           4 :         notifyListeners();
     176             :         return;
     177             :       }
     178             :     }
     179           5 :     if (_isDestinationMatched(destination)) {
     180           5 :       _updateStack(destination);
     181           5 :       notifyListeners();
     182             :     } else {
     183           2 :       if (notifyOnError) {
     184           4 :         _error = NavigationControllerError(destination: destination);
     185           2 :         notifyListeners();
     186             :         return;
     187             :       } else {
     188           2 :         throw UnknownDestinationException(destination);
     189             :       }
     190             :     }
     191             :   }
     192             : 
     193             :   /// Closes the current destination.
     194             :   ///
     195             :   /// The current destination is removed from the navigation [stack].
     196             :   ///
     197             :   /// If it is the only destination in the stack, it remains in the stack and
     198             :   /// [shouldClose] flag is set to 'true'.
     199             :   ///
     200           4 :   void goBack() {
     201           4 :     _gotBack = true;
     202          12 :     if (_stack.length > 1) {
     203           8 :       _stack.removeLast();
     204           4 :       _shouldClose = false;
     205             :     } else {
     206           4 :       _shouldClose = true;
     207             :     }
     208           8 :     Log.d(_tag,
     209          16 :         'goBack(): destination=${_stack.last}, shouldClose=$_shouldClose');
     210           4 :     notifyListeners();
     211             :   }
     212             : 
     213           5 :   bool _isDestinationMatched(Destination destination) =>
     214          25 :       destinations.any((element) => element.isMatch(destination.uri));
     215             : 
     216           5 :   void _updateStack(Destination destination) {
     217          10 :     if (destination.configuration.reset) {
     218           4 :       _stack.clear();
     219             :     } else {
     220          15 :       if (destination.configuration.action == DestinationAction.replace) {
     221           0 :         _stack.removeLast();
     222             :       }
     223             :     }
     224           5 :     final upwardStack = _buildUpwardStack(destination);
     225           5 :     if (upwardStack.isNotEmpty) {
     226             :       // Find first missing item of upward stack
     227             :       int startUpwardFrom = 0;
     228           6 :       for (int i = 0; i < upwardStack.length; i++) {
     229           4 :         if (_stack.isNotEmpty && _stack.last == upwardStack[i]) {
     230           0 :           startUpwardFrom = i + 1;
     231             :         }
     232             :       }
     233             :       // Add all missing upward destinations to the stack
     234           4 :       if (startUpwardFrom < upwardStack.length) {
     235           6 :         for (int i = startUpwardFrom; i < upwardStack.length; i++) {
     236           6 :           _stack.addLast(upwardStack[i]);
     237             :         }
     238             :       }
     239             :     }
     240          10 :     _stack.addLast(destination);
     241             :   }
     242             : 
     243           5 :   List<Destination> _buildUpwardStack(Destination destination) {
     244           5 :     final result = <Destination>[];
     245           5 :     var upwardDestination = destination.upwardDestination;
     246             :     while (upwardDestination != null) {
     247           2 :       result.insert(0, upwardDestination);
     248           2 :       upwardDestination = upwardDestination.upwardDestination;
     249             :     }
     250             :     return result;
     251             :   }
     252             : }
     253             : 
     254             : /// Contains navigation error details
     255             : ///
     256             : class NavigationControllerError {
     257             :   /// Creates an error object
     258           2 :   NavigationControllerError({
     259             :     this.destination,
     260             :   });
     261             : 
     262             :   /// A destination related to this error
     263             :   ///
     264             :   final Destination? destination;
     265             : 
     266           0 :   @override
     267           0 :   String toString() => '$runtimeType={destination: $destination}';
     268             : }

Generated by: LCOV version