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

          Line data    Source code
       1             : import 'dart:collection';
       2             : import 'package:flutter/foundation.dart';
       3             : import 'package:flutter/widgets.dart';
       4             : 
       5             : import 'destination.dart';
       6             : import 'exceptions.dart';
       7             : import 'utils/utils.dart';
       8             : import 'widgets/index.dart';
       9             : 
      10             : /// A [NavigationController] manages the navigation state.
      11             : ///
      12             : /// Using the given [destinations] list, it maintains the navigation [stack].
      13             : ///
      14             : /// The navigation stack is updated when a user navigates to specified destination
      15             : /// by calling [goTo] method, or returns back with [goBack] method.
      16             : ///
      17             : /// The navigation controller (navigator) provides an access to a [currentDestination],
      18             : /// which is one on the top of the stack.
      19             : ///
      20             : /// Initially, the navigation stack contains a destination at [initialDestinationIndex]
      21             : /// in the provided list of destinations.
      22             : ///
      23             : /// [NavigationController] implements [ChangeNotifier] and notifies its listener when
      24             : /// the [currentDestination]/[stack] is changed, or some error was happened.
      25             : ///
      26             : /// See also:
      27             : /// - [Destination]
      28             : /// - [NavigationScheme]
      29             : /// - [NavigationControllerError]
      30             : ///
      31             : class NavigationController with ChangeNotifier {
      32             :   /// Creates navigation controller instance.
      33             :   ///
      34             :   /// Add initial destination to the navigation stack and creates a [GlobalKey] for
      35             :   /// a [Navigator] widget.
      36             :   ///
      37           7 :   NavigationController({
      38             :     required this.destinations,
      39             :     this.builder = const DefaultNavigatorBuilder(),
      40             :     this.initialDestinationIndex = 0,
      41             :     this.notifyOnError = true,
      42             :     this.tag = '',
      43             :   }) {
      44          35 :     _stack.add(destinations[initialDestinationIndex]);
      45          21 :     key = GlobalKey<NavigatorState>(debugLabel: tag);
      46             :   }
      47             : 
      48             :   /// List of destinations, which this navigator operate of.
      49             :   ///
      50             :   final List<Destination> destinations;
      51             : 
      52             :   /// An implementation of [NavigatorBuilder] which creates a navigation UI.
      53             :   ///
      54             :   /// Defaults to [DefaultNavigatorBuilder] which uses Flutter's [Navigator] widget
      55             :   /// to represent the stack of destinations.
      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             :   Destination? _backFrom;
     100             : 
     101             :   /// The destination from [goBack] action is performed.
     102             :   ///
     103             :   /// It is set to current destination right before [goBack] action is processed.
     104             :   /// Otherwise it is set to null.
     105             :   ///
     106          10 :   Destination? get backFrom => _backFrom;
     107             : 
     108             :   NavigationControllerError? _error;
     109             : 
     110             :   /// Error details
     111             :   ///
     112          10 :   NavigationControllerError? get error => _error;
     113             : 
     114             :   /// Whether an error was happened on last [goTo] or [goBack] action.
     115             :   ///
     116           8 :   bool get hasError => _error != null;
     117             : 
     118             :   /// Indicates if persisting of navigation state in destination parameters is needed.
     119             :   ///
     120             :   /// When it is *true*, the following is happened on navigation to a destination:
     121             :   /// - If [DestinationSettings.reset] is not set in the requested destination,
     122             :   /// the current navigation state is saved in the requested destination parameters.
     123             :   /// Particularly, the [currentDestination] is saved in the [DestinationParameters.upwardParameterName]
     124             :   /// parameter, and current destination of each nested [NavigationController]
     125             :   /// is saved in the [DestinationParameters.nestedParameterName] parameter of the requested destination.
     126             :   /// - If [DestinationSettings.reset] is *true*, the navigation state is restored
     127             :   /// from the requested destination parameters.
     128             :   ///
     129           5 :   bool get keepStateInParameters =>
     130          15 :       builder.keepStateInParameters == KeepingStateInParameters.always ||
     131          15 :       builder.keepStateInParameters == KeepingStateInParameters.auto && kIsWeb;
     132             : 
     133             :   bool _shouldClose = false;
     134             : 
     135             :   /// Whether the navigator should close.
     136             :   ///
     137             :   /// It is set to 'true' when user call [goBack] method when the only destination
     138             :   /// is in the stack.
     139             :   ///
     140             :   /// If this is the root navigator in the [NavigationScheme], setting [shouldClose]
     141             :   /// to true will cause closing the app.
     142             :   ///
     143          12 :   bool get shouldClose => _shouldClose;
     144             : 
     145             :   final _stack = Queue<Destination>();
     146             : 
     147          20 :   String get _tag => '$runtimeType::$tag';
     148             : 
     149             :   /// The current destination of the navigator.
     150             :   ///
     151             :   /// It is the topmost destination in the navigation [stack].
     152             :   ///
     153          18 :   Destination get currentDestination => _stack.last;
     154             : 
     155             :   /// The navigation [stack].
     156             :   ///
     157             :   /// When [goTo] method is called, the destination is placed on the top of the stack,
     158             :   /// and when [goBack] method is called, the topmost destination is removed from
     159             :   /// the stack.
     160             :   ///
     161          18 :   List<Destination> get stack => _stack.toList();
     162             : 
     163             :   /// Builds a widget that wraps destinations of the navigator.
     164             :   ///
     165           3 :   Widget build(BuildContext context) {
     166           6 :     return builder.build(context, this);
     167             :   }
     168             : 
     169             :   /// Opens specified destination.
     170             :   ///
     171             :   /// By calling calling this method, depending on [destination.settings],
     172             :   /// the given destination will be either added to the top of the navigation [stack],
     173             :   /// or will replace the topmost destination in the stack.
     174             :   ///
     175             :   /// Also, missing upward destinations can be added to the stack, if the
     176             :   /// current stack state doesn't match, and the [destination.upwardDestinationBuilder]
     177             :   /// is defined.
     178             :   ///
     179             :   /// Throws [UnknownDestinationException] if the navigator's [destinations]
     180             :   /// doesn't contain given destination.
     181             :   ///
     182           5 :   Future<void> goTo(Destination destination) async {
     183          10 :     Log.d(_tag,
     184          15 :         'goTo(): destination=$destination, reset=${destination.settings.reset}');
     185           5 :     _backFrom = null;
     186           5 :     _error = null;
     187           5 :     _shouldClose = false;
     188          10 :     if (currentDestination == destination) {
     189           6 :       if (!destination.settings.reset) {
     190           6 :         Log.d(_tag,
     191             :             'goTo(): The destination is already on top. No action required.');
     192           3 :         notifyListeners();
     193             :         return;
     194             :       }
     195             :     }
     196           5 :     if (_isDestinationMatched(destination)) {
     197          10 :       await _updateStack(destination);
     198           5 :       notifyListeners();
     199             :     } else {
     200           2 :       if (notifyOnError) {
     201           4 :         _error = NavigationControllerError(destination: destination);
     202           2 :         notifyListeners();
     203             :         return;
     204             :       } else {
     205           2 :         throw UnknownDestinationException(destination);
     206             :       }
     207             :     }
     208             :   }
     209             : 
     210             :   /// Closes the current destination.
     211             :   ///
     212             :   /// The topmost destination is removed from the navigation [stack].
     213             :   ///
     214             :   /// If it is the only destination in the stack, it remains in the stack and
     215             :   /// [shouldClose] flag is set to 'true'.
     216             :   ///
     217           4 :   void goBack() {
     218           8 :     _backFrom = currentDestination;
     219          12 :     if (_stack.length > 1) {
     220           8 :       _stack.removeLast();
     221           4 :       _shouldClose = false;
     222             :     } else {
     223           4 :       _shouldClose = true;
     224             :     }
     225           8 :     Log.d(_tag,
     226          16 :         'goBack(): destination=${_stack.last}, shouldClose=$_shouldClose');
     227           4 :     notifyListeners();
     228             :   }
     229             : 
     230           2 :   void resetStack(List<Destination> destinations) {
     231           4 :     _stack.clear();
     232           4 :     for (final destination in destinations) {
     233           4 :       _stack.add(destination);
     234             :     }
     235             :   }
     236             : 
     237           5 :   bool _isDestinationMatched(Destination destination) =>
     238          25 :       destinations.any((element) => element.isMatch(destination.uri));
     239             : 
     240           5 :   Future<void> _updateStack(Destination destination) async {
     241          10 :     if (destination.settings.reset) {
     242           6 :       _stack.clear();
     243             :     } else {
     244          15 :       if (destination.settings.action == DestinationAction.replace) {
     245           4 :         _stack.removeLast();
     246             :       }
     247             :     }
     248          10 :     final upwardStack = await _buildUpwardStack(destination);
     249           5 :     if (upwardStack.isNotEmpty) {
     250             :       // Find first missing item of upward stack
     251             :       int startUpwardFrom = 0;
     252           9 :       for (int i = 0; i < upwardStack.length; i++) {
     253          14 :         if (_stack.isNotEmpty && _stack.last == upwardStack[i]) {
     254           2 :           startUpwardFrom = i + 1;
     255             :         }
     256             :       }
     257             :       // Add all missing upward destinations to the stack
     258           6 :       if (startUpwardFrom < upwardStack.length) {
     259           9 :         for (int i = startUpwardFrom; i < upwardStack.length; i++) {
     260           9 :           _stack.addLast(upwardStack[i]);
     261             :         }
     262             :       }
     263             :     }
     264          10 :     _stack.addLast(destination);
     265             :   }
     266             : 
     267           5 :   Future<List<Destination>> _buildUpwardStack(Destination destination) async {
     268           5 :     final result = <Destination>[];
     269          10 :     var upwardDestination = await destination.upwardDestination;
     270             :     while (upwardDestination != null) {
     271           3 :       result.insert(0, upwardDestination);
     272           6 :       upwardDestination = await upwardDestination.upwardDestination;
     273             :     }
     274             :     return result;
     275             :   }
     276             : }
     277             : 
     278             : /// Automatic persisting of navigation state.
     279             : ///
     280             : /// Once persisting of navigation state in destination parameters is enabled,
     281             : /// the current stack will be serialized and saved in the [DestinationParameters.stateParameterName]
     282             : /// parameter on navigation to a destination.
     283             : /// When the destination with persisted navigation state is requested by the platform,
     284             : /// the navigation stack will be deserialized from the parameter and explicitly set in the
     285             : /// navigation controller.
     286             : ///
     287             : /// Basically, persisting of navigation state in destination parameters make sense in web apps,
     288             : /// to be able to restore arbitrary navigation stack when the user navigates to a destination
     289             : /// through the browser history or a deeplink.
     290             : /// To support this, the [auto] option is used in [NavigatorBuilder] by default.
     291             : ///
     292             : /// When automatic persisting of navigation state is disabled,
     293             : /// you still able to implement your custom logic manually, by providing proper [Destination.upwardDestinationBuilder].
     294             : ///
     295          11 : enum KeepingStateInParameters {
     296             :   /// The navigation state will be always kept
     297             :   ///
     298             :   always,
     299             : 
     300             :   /// The navigation state will be only kept when the app is running on the Web platform.
     301             :   ///
     302             :   auto,
     303             : 
     304             :   /// The navigation state will not be kept automatically.
     305             :   ///
     306             :   none,
     307             : }
     308             : 
     309             : /// Contains navigation error details
     310             : ///
     311             : class NavigationControllerError {
     312             :   /// Creates an error object
     313           2 :   NavigationControllerError({
     314             :     this.destination,
     315             :   });
     316             : 
     317             :   /// A destination related to this error
     318             :   ///
     319             :   final Destination? destination;
     320             : 
     321           2 :   @override
     322           6 :   String toString() => '$runtimeType: destination=$destination';
     323             : }

Generated by: LCOV version