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

Generated by: LCOV version