Ahmed Fwela

Complete rework of the navigation system (still backward compatible)

1 -import 'package:example_nav2/app/modules/home/views/dashboard_view.dart';  
2 -import 'package:example_nav2/app/routes/app_pages.dart';  
3 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
4 -  
5 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
6 -import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';  
7 -import 'package:get/get_navigation/src/nav2/router_outlet.dart';  
8 3
  4 +import '../../../routes/app_pages.dart';
9 import '../controllers/home_controller.dart'; 5 import '../controllers/home_controller.dart';
  6 +import 'dashboard_view.dart';
10 7
11 class HomeView extends GetView<HomeController> { 8 class HomeView extends GetView<HomeController> {
12 @override 9 @override
@@ -14,26 +11,21 @@ class HomeView extends GetView<HomeController> { @@ -14,26 +11,21 @@ class HomeView extends GetView<HomeController> {
14 return GetRouterOutlet.builder( 11 return GetRouterOutlet.builder(
15 builder: (context, delegate, currentRoute) { 12 builder: (context, delegate, currentRoute) {
16 //This router outlet handles the appbar and the bottom navigation bar 13 //This router outlet handles the appbar and the bottom navigation bar
17 - final title = currentRoute?.title;  
18 - final currentName = currentRoute?.name; 14 + final currentLocation = currentRoute?.location;
19 var currentIndex = 0; 15 var currentIndex = 0;
20 - if (currentName?.startsWith(Routes.PRODUCTS) == true) currentIndex = 2;  
21 - if (currentName?.startsWith(Routes.PROFILE) == true) currentIndex = 1; 16 + if (currentLocation?.startsWith(Routes.PRODUCTS) == true) {
  17 + currentIndex = 2;
  18 + }
  19 + if (currentLocation?.startsWith(Routes.PROFILE) == true) {
  20 + currentIndex = 1;
  21 + }
22 return Scaffold( 22 return Scaffold(
23 - appBar: title == null  
24 - ? null  
25 - : AppBar(  
26 - title: Text(title),  
27 - centerTitle: true,  
28 - ),  
29 body: GetRouterOutlet( 23 body: GetRouterOutlet(
30 emptyPage: (delegate) => DashboardView(), 24 emptyPage: (delegate) => DashboardView(),
31 pickPages: (currentNavStack) { 25 pickPages: (currentNavStack) {
32 // will take any route after home 26 // will take any route after home
33 - final res = currentNavStack.pickAfterRoute(Routes.HOME);  
34 - // print('''RouterOutlet rebuild:  
35 - // currentStack: $currentNavStack  
36 - // pickedStack: $res'''); 27 + final res =
  28 + currentNavStack.currentTreeBranch.pickAfterRoute(Routes.HOME);
37 return res; 29 return res;
38 }, 30 },
39 ), 31 ),
@@ -42,7 +34,7 @@ class HomeView extends GetView<HomeController> { @@ -42,7 +34,7 @@ class HomeView extends GetView<HomeController> {
42 onTap: (value) { 34 onTap: (value) {
43 switch (value) { 35 switch (value) {
44 case 0: 36 case 0:
45 - delegate.offUntil(Routes.HOME); 37 + delegate.until(Routes.HOME);
46 break; 38 break;
47 case 1: 39 case 1:
48 delegate.toNamed(Routes.PROFILE); 40 delegate.toNamed(Routes.PROFILE);
  1 +import 'package:get/get.dart';
  2 +
  3 +import '../controllers/root_controller.dart';
  4 +
  5 +class RootBinding extends Bindings {
  6 + @override
  7 + void dependencies() {
  8 + Get.lazyPut<RootController>(
  9 + () => RootController(),
  10 + );
  11 + }
  12 +}
  1 +import 'package:get/get.dart';
  2 +
  3 +class RootController extends GetxController {
  4 + //TODO: Implement RootController
  5 +
  6 + final count = 0.obs;
  7 + @override
  8 + void onInit() {
  9 + super.onInit();
  10 + }
  11 +
  12 + @override
  13 + void onReady() {
  14 + super.onReady();
  15 + }
  16 +
  17 + @override
  18 + void onClose() {}
  19 + void increment() => count.value++;
  20 +}
  1 +import 'package:example_nav2/app/routes/app_pages.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:get/get.dart';
  4 +
  5 +class DrawerWidget extends StatelessWidget {
  6 + const DrawerWidget({
  7 + Key? key,
  8 + }) : super(key: key);
  9 +
  10 + @override
  11 + Widget build(BuildContext context) {
  12 + return Drawer(
  13 + child: Column(
  14 + children: [
  15 + Container(
  16 + height: 100,
  17 + color: Colors.red,
  18 + ),
  19 + ListTile(
  20 + title: Text('Home'),
  21 + onTap: () {
  22 + Get.getDelegate()?.toNamed(Routes.HOME);
  23 + //to close the drawer
  24 +
  25 + Navigator.of(context).pop();
  26 + },
  27 + ),
  28 + ListTile(
  29 + title: Text('Settings'),
  30 + onTap: () {
  31 + Get.getDelegate()?.toNamed(Routes.SETTINGS);
  32 + //to close the drawer
  33 +
  34 + Navigator.of(context).pop();
  35 + },
  36 + ),
  37 + ],
  38 + ),
  39 + );
  40 + }
  41 +}
  1 +import 'package:example_nav2/app/routes/app_pages.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'package:get/get.dart';
  5 +
  6 +import '../controllers/root_controller.dart';
  7 +import 'drawer.dart';
  8 +
  9 +class RootView extends GetView<RootController> {
  10 + @override
  11 + Widget build(BuildContext context) {
  12 + return GetRouterOutlet.builder(
  13 + builder: (context, rDelegate, currentRoute) {
  14 + final title = currentRoute?.location;
  15 + return Scaffold(
  16 + drawer: DrawerWidget(),
  17 + appBar: AppBar(
  18 + title: Text(title ?? ''),
  19 + centerTitle: true,
  20 + ),
  21 + body: GetRouterOutlet(
  22 + emptyPage: (delegate) {
  23 + return Center(
  24 + child: Column(
  25 + mainAxisSize: MainAxisSize.min,
  26 + children: [
  27 + Text('<<<< Select something from the drawer on the left'),
  28 + Builder(
  29 + builder: (context) => MaterialButton(
  30 + child: Icon(Icons.open_in_new_outlined),
  31 + onPressed: () {
  32 + Scaffold.of(context).openDrawer();
  33 + },
  34 + ),
  35 + )
  36 + ],
  37 + ),
  38 + );
  39 + },
  40 + pickPages: (currentNavStack) {
  41 + //show all routes here except the root view
  42 + print('Root RouterOutlet: $currentNavStack');
  43 + return currentNavStack.currentTreeBranch.skip(1).take(1).toList();
  44 + },
  45 + ),
  46 + );
  47 + },
  48 + );
  49 + }
  50 +}
@@ -8,10 +8,6 @@ class SettingsView extends GetView<SettingsController> { @@ -8,10 +8,6 @@ class SettingsView extends GetView<SettingsController> {
8 @override 8 @override
9 Widget build(BuildContext context) { 9 Widget build(BuildContext context) {
10 return Scaffold( 10 return Scaffold(
11 - appBar: AppBar(  
12 - title: Text('SettingsView'),  
13 - centerTitle: true,  
14 - ),  
15 body: Center( 11 body: Center(
16 child: Text( 12 child: Text(
17 'SettingsView is working', 13 'SettingsView is working',
  1 +import 'package:example_nav2/app/modules/root/bindings/root_binding.dart';
  2 +import 'package:example_nav2/app/modules/root/views/root_view.dart';
1 import 'package:get/get.dart'; 3 import 'package:get/get.dart';
2 import 'package:get/get_navigation/src/nav2/router_outlet.dart'; 4 import 'package:get/get_navigation/src/nav2/router_outlet.dart';
3 import '../modules/home/bindings/home_binding.dart'; 5 import '../modules/home/bindings/home_binding.dart';
@@ -20,15 +22,21 @@ class AppPages { @@ -20,15 +22,21 @@ class AppPages {
20 22
21 static final routes = [ 23 static final routes = [
22 GetPage( 24 GetPage(
  25 + name: '/',
  26 + page: () => RootView(),
  27 + middlewares: [
  28 + RouterOutletContainerMiddleWare('/'),
  29 + ],
  30 + binding: RootBinding(),
  31 + children: [
  32 + GetPage(
23 name: _Paths.HOME, 33 name: _Paths.HOME,
  34 + preventDuplicates: true,
24 page: () => HomeView(), 35 page: () => HomeView(),
25 bindings: [ 36 bindings: [
26 HomeBinding(), 37 HomeBinding(),
27 ], 38 ],
28 title: null, 39 title: null,
29 - middlewares: [  
30 - RouterOutletContainerMiddleWare(_Paths.HOME),  
31 - ],  
32 children: [ 40 children: [
33 GetPage( 41 GetPage(
34 name: _Paths.PROFILE, 42 name: _Paths.PROFILE,
@@ -56,5 +64,7 @@ class AppPages { @@ -56,5 +64,7 @@ class AppPages {
56 page: () => SettingsView(), 64 page: () => SettingsView(),
57 binding: SettingsBinding(), 65 binding: SettingsBinding(),
58 ), 66 ),
  67 + ],
  68 + ),
59 ]; 69 ];
60 } 70 }
@@ -10,8 +10,14 @@ void main() { @@ -10,8 +10,14 @@ void main() {
10 GetMaterialApp.router( 10 GetMaterialApp.router(
11 title: "Application", 11 title: "Application",
12 getPages: AppPages.routes, 12 getPages: AppPages.routes,
13 - routeInformationParser: GetInformationParser(),  
14 - routerDelegate: GetDelegate(), 13 + routeInformationParser: GetInformationParser(
  14 + // initialRoute: Routes.HOME,
  15 + ),
  16 + routerDelegate: GetDelegate(
  17 + backButtonPopMode: PopMode.History,
  18 + preventDuplicateHandlingMode:
  19 + PreventDuplicateHandlingMode.PopUntilOriginalRoute,
  20 + ),
15 ), 21 ),
16 ); 22 );
17 } 23 }
1 import 'package:flutter/widgets.dart'; 1 import 'package:flutter/widgets.dart';
2 -import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';  
3 2
4 import '../../get_core/src/get_interface.dart'; 3 import '../../get_core/src/get_interface.dart';
5 -  
6 import '../../route_manager.dart'; 4 import '../../route_manager.dart';
7 import 'get_instance.dart'; 5 import 'get_instance.dart';
8 6
@@ -130,5 +128,5 @@ extension Inst on GetInterface { @@ -130,5 +128,5 @@ extension Inst on GetInterface {
130 TDelegate? delegate<TDelegate extends RouterDelegate<TPage>, TPage>() => 128 TDelegate? delegate<TDelegate extends RouterDelegate<TPage>, TPage>() =>
131 routerDelegate as TDelegate?; 129 routerDelegate as TDelegate?;
132 130
133 - GetDelegate? getDelegate() => delegate<GetDelegate, GetPage>(); 131 + GetDelegate? getDelegate() => delegate<GetDelegate, GetNavConfig>();
134 } 132 }
@@ -3,6 +3,9 @@ library get_navigation; @@ -3,6 +3,9 @@ library get_navigation;
3 export 'src/bottomsheet/bottomsheet.dart'; 3 export 'src/bottomsheet/bottomsheet.dart';
4 export 'src/extension_navigation.dart'; 4 export 'src/extension_navigation.dart';
5 export 'src/nav2/get_information_parser.dart'; 5 export 'src/nav2/get_information_parser.dart';
  6 +export 'src/nav2/get_nav_config.dart';
  7 +export 'src/nav2/get_router_delegate.dart';
  8 +export 'src/nav2/router_outlet.dart';
6 export 'src/root/get_cupertino_app.dart'; 9 export 'src/root/get_cupertino_app.dart';
7 export 'src/root/get_material_app.dart'; 10 export 'src/root/get_material_app.dart';
8 export 'src/root/internacionalization.dart'; 11 export 'src/root/internacionalization.dart';
@@ -2,28 +2,44 @@ import 'package:flutter/foundation.dart'; @@ -2,28 +2,44 @@ import 'package:flutter/foundation.dart';
2 import 'package:flutter/widgets.dart'; 2 import 'package:flutter/widgets.dart';
3 import '../../../get.dart'; 3 import '../../../get.dart';
4 4
5 -class GetInformationParser extends RouteInformationParser<GetPage> { 5 +class GetInformationParser extends RouteInformationParser<GetNavConfig> {
  6 + final String initialRoute;
  7 +
  8 + GetInformationParser({
  9 + this.initialRoute = '/',
  10 + });
6 @override 11 @override
7 - SynchronousFuture<GetPage> parseRouteInformation( 12 + SynchronousFuture<GetNavConfig> parseRouteInformation(
8 RouteInformation routeInformation, 13 RouteInformation routeInformation,
9 ) { 14 ) {
10 - if (routeInformation.location == '/') {  
11 - return SynchronousFuture(Get.routeTree.routes.first); 15 + print('GetInformationParser: route location: ${routeInformation.location}');
  16 + var location = routeInformation.location;
  17 + if (location == '/') {
  18 + //check if there is a corresponding page
  19 + //if not, relocate to initialRoute
  20 + if (!Get.routeTree.routes.any((element) => element.name == '/')) {
  21 + location = initialRoute;
  22 + }
12 } 23 }
13 - print('route location: ${routeInformation.location}');  
14 - final page = Get.routeTree.matchRoute(routeInformation.location!);  
15 - print(page.parameters);  
16 - final val = page.route!.copy(  
17 - name: routeInformation.location,  
18 - parameter: Map.from(page.parameters), 24 +
  25 + final matchResult = Get.routeTree.matchRoute(location ?? initialRoute);
  26 +
  27 + return SynchronousFuture(
  28 + GetNavConfig(
  29 + currentTreeBranch: matchResult.treeBranch,
  30 + location: location,
  31 + state: routeInformation.state,
  32 + ),
19 ); 33 );
20 - return SynchronousFuture(val);  
21 } 34 }
22 35
23 @override 36 @override
24 - RouteInformation restoreRouteInformation(GetPage uri) {  
25 - print('restore $uri'); 37 + RouteInformation restoreRouteInformation(GetNavConfig config) {
  38 + print('restore $config');
26 39
27 - return RouteInformation(location: uri.name); 40 + return RouteInformation(
  41 + location: config.location,
  42 + state: config.state,
  43 + );
28 } 44 }
29 } 45 }
  1 +import 'package:flutter/widgets.dart';
  2 +
  3 +import 'package:get/get.dart';
  4 +
  5 +/// This config enables us to navigate directly to a sub-url
  6 +class GetNavConfig extends RouteInformation {
  7 + final List<GetPage> currentTreeBranch;
  8 + GetPage? get currentPage => currentTreeBranch.last;
  9 +
  10 + GetNavConfig({
  11 + required this.currentTreeBranch,
  12 + required String? location,
  13 + required Object? state,
  14 + }) : super(
  15 + location: location,
  16 + state: state,
  17 + );
  18 +
  19 + GetNavConfig copyWith({
  20 + List<GetPage>? currentTreeBranch,
  21 + GetPage? currentPage,
  22 + required String? location,
  23 + required Object? state,
  24 + }) {
  25 + return GetNavConfig(
  26 + currentTreeBranch: currentTreeBranch ?? this.currentTreeBranch,
  27 + location: location ?? this.location,
  28 + state: state ?? this.state,
  29 + );
  30 + }
  31 +
  32 + @override
  33 + String toString() => '''
  34 + ======GetNavConfig=====
  35 + currentTreeBranch: $currentTreeBranch
  36 + currentPage: $currentPage
  37 + ======GetNavConfig=====''';
  38 +}
1 import 'dart:async'; 1 import 'dart:async';
2 2
  3 +import 'package:flutter/foundation.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
4 import 'package:get/get_navigation/src/nav2/router_outlet.dart'; 5 import 'package:get/get_navigation/src/nav2/router_outlet.dart';
5 import '../../../get.dart'; 6 import '../../../get.dart';
6 import '../../../get_state_manager/src/simple/list_notifier.dart'; 7 import '../../../get_state_manager/src/simple/list_notifier.dart';
7 8
8 -class GetDelegate extends RouterDelegate<GetPage>  
9 - with ListenableMixin, ListNotifierMixin {  
10 - final List<GetPage> routes = <GetPage>[]; 9 +/// Enables the user to customize the intended pop behavior
  10 +///
  11 +/// Goes to either the previous history entry or the previous page entry
  12 +///
  13 +/// e.g. if the user navigates to these pages
  14 +/// 1) /home
  15 +/// 2) /home/products/1234
  16 +///
  17 +/// when popping on [History] mode, it will emulate a browser back button.
  18 +///
  19 +/// so the new history stack will be:
  20 +/// 1) /home
  21 +///
  22 +/// when popping on [Page] mode, it will only remove the last part of the route
  23 +/// so the new history stack will be:
  24 +/// 1) /home
  25 +/// 2) /home/products
  26 +///
  27 +/// another pop will change the history stack to:
  28 +/// 1) /home
  29 +enum PopMode {
  30 + History,
  31 + Page,
  32 +}
  33 +
  34 +/// Enables the user to customize the behavior when pushing multiple routes that
  35 +/// shouldn't be duplicates
  36 +enum PreventDuplicateHandlingMode {
  37 + /// Removes the history entries until it reaches the old route
  38 + PopUntilOriginalRoute,
11 39
12 - final pageRoutes = <GetPage, GetPageRoute>{}; 40 + /// Simply don't push the new route
  41 + DoNothing,
  42 +}
  43 +
  44 +class GetDelegate extends RouterDelegate<GetNavConfig>
  45 + with ListenableMixin, ListNotifierMixin {
  46 + final List<GetNavConfig> history = <GetNavConfig>[];
  47 + final PopMode backButtonPopMode;
  48 + final PreventDuplicateHandlingMode preventDuplicateHandlingMode;
  49 + final pageRoutes = <String, GetPageRoute>{};
13 50
14 GetPage? notFoundRoute; 51 GetPage? notFoundRoute;
15 52
16 - final List<NavigatorObserver>? dipNavObservers; 53 + final List<NavigatorObserver>? navigatorObservers;
17 final TransitionDelegate<dynamic>? transitionDelegate; 54 final TransitionDelegate<dynamic>? transitionDelegate;
  55 + final _resultCompleter = <GetNavConfig, Completer<Object?>>{};
18 56
19 GlobalKey<NavigatorState> get navigatorKey => 57 GlobalKey<NavigatorState> get navigatorKey =>
20 GetNavigation.getxController.key; 58 GetNavigation.getxController.key;
21 59
22 - GetDelegate(  
23 - {this.notFoundRoute, this.dipNavObservers, this.transitionDelegate}); 60 + GetDelegate({
  61 + this.notFoundRoute,
  62 + this.navigatorObservers,
  63 + this.transitionDelegate,
  64 + this.backButtonPopMode = PopMode.History,
  65 + this.preventDuplicateHandlingMode = PreventDuplicateHandlingMode.DoNothing,
  66 + });
  67 +
  68 + /// Adds a new history entry and waits for the result
  69 + Future<T?> pushHistory<T>(
  70 + GetNavConfig config, {
  71 + bool rebuildStack = true,
  72 + }) {
  73 + //this changes the currentConfiguration
  74 + final completer = Completer<T?>();
  75 + _resultCompleter[config] = completer;
  76 + _pushHistory(config);
  77 + if (rebuildStack) {
  78 + refresh();
  79 + }
  80 + return completer.future;
  81 + }
  82 +
  83 + void _removeHistoryEntry(GetNavConfig entry) {
  84 + history.remove(entry);
  85 + pageRoutes.remove(entry.location);
  86 + final lastCompleter = _resultCompleter.remove(entry);
  87 + lastCompleter?.complete(entry);
  88 + }
  89 +
  90 + void _pushHistory(GetNavConfig config) {
  91 + if (config.currentPage!.preventDuplicates) {
  92 + if (history.any((element) => element.location == config.location)) {
  93 + switch (preventDuplicateHandlingMode) {
  94 + case PreventDuplicateHandlingMode.PopUntilOriginalRoute:
  95 + until(config.location!, popMode: PopMode.History);
  96 + return;
  97 + case PreventDuplicateHandlingMode.DoNothing:
  98 + default:
  99 + return;
  100 + }
  101 + }
  102 + }
  103 + history.add(config);
  104 + pageRoutes[config.location!] =
  105 + PageRedirect(config.currentPage!, _notFound()).page();
  106 + }
  107 +
  108 + GetNavConfig? _popHistory() {
  109 + if (!_canPopHistory()) return null;
  110 + return _doPopHistory();
  111 + }
  112 +
  113 + GetNavConfig _doPopHistory() {
  114 + final res = history.removeLast();
  115 + pageRoutes.remove(res.location);
  116 + return res;
  117 + }
  118 +
  119 + GetNavConfig? _popPage() {
  120 + if (!_canPopPage()) return null;
  121 + return _doPopPage();
  122 + }
  123 +
  124 + GetNavConfig? _pop(PopMode mode) {
  125 + switch (mode) {
  126 + case PopMode.History:
  127 + return _popHistory();
  128 + case PopMode.Page:
  129 + return _popPage();
  130 + default:
  131 + return null;
  132 + }
  133 + }
  134 +
  135 + // returns the popped page
  136 + GetNavConfig? _doPopPage() {
  137 + final currentBranch = currentConfiguration?.currentTreeBranch;
  138 + if (currentBranch != null && currentBranch.length > 1) {
  139 + //remove last part only
  140 + final remaining = currentBranch.take(currentBranch.length - 1);
  141 + final prevHistoryEntry =
  142 + history.length > 1 ? history[history.length - 2] : null;
  143 +
  144 + //check if current route is the same as the previous route
  145 + if (prevHistoryEntry != null) {
  146 + //if so, pop the entire history entry
  147 + final newLocation = remaining.last.name;
  148 + final prevLocation = prevHistoryEntry.location;
  149 + if (newLocation == prevLocation) {
  150 + //pop the entire history entry
  151 + return _popHistory();
  152 + }
  153 + }
  154 +
  155 + //create a new route with the remaining tree branch
  156 + final res = _popHistory();
  157 + _pushHistory(
  158 + GetNavConfig(
  159 + currentTreeBranch: remaining.toList(),
  160 + location: remaining.last.name,
  161 + state: null, //TOOD: persist state??
  162 + ),
  163 + );
  164 + return res;
  165 + } else {
  166 + //remove entire entry
  167 + return _popHistory();
  168 + }
  169 + }
  170 +
  171 + Future<GetNavConfig?> popHistory() {
  172 + return SynchronousFuture(_popHistory());
  173 + }
  174 +
  175 + bool _canPopHistory() {
  176 + return history.length > 1;
  177 + }
  178 +
  179 + Future<bool> canPopHistory() {
  180 + return SynchronousFuture(_canPopHistory());
  181 + }
24 182
25 - List<GetPage> getVisiblePages() {  
26 - return routes.where((r) { 183 + bool _canPopPage() {
  184 + final currentTreeBranch = currentConfiguration?.currentTreeBranch;
  185 + if (currentTreeBranch == null) return false;
  186 + return currentTreeBranch.length > 1 ? true : _canPopHistory();
  187 + }
  188 +
  189 + Future<bool> canPopPage() {
  190 + return SynchronousFuture(_canPopPage());
  191 + }
  192 +
  193 + /// gets the visual pages from the current history entry
  194 + ///
  195 + /// visual pages must have the [RouterOutletContainerMiddleWare] middleware
  196 + /// with `stayAt` equal to the route name of the visual page
  197 + List<GetPage> getVisualPages() {
  198 + final currentHistory = currentConfiguration;
  199 + if (currentHistory == null) return <GetPage>[];
  200 + return currentHistory.currentTreeBranch.where((r) {
27 final mware = 201 final mware =
28 (r.middlewares ?? []).whereType<RouterOutletContainerMiddleWare>(); 202 (r.middlewares ?? []).whereType<RouterOutletContainerMiddleWare>();
29 if (mware.length == 0) return true; 203 if (mware.length == 0) return true;
@@ -31,65 +205,77 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -31,65 +205,77 @@ class GetDelegate extends RouterDelegate<GetPage>
31 }).toList(); 205 }).toList();
32 } 206 }
33 207
34 - /// Called by the [Router] at startup with the structure that the  
35 - /// [RouteInformationParser] obtained from parsing the initial route.  
36 @override 208 @override
37 Widget build(BuildContext context) { 209 Widget build(BuildContext context) {
38 - final pages = getVisiblePages(); 210 + final pages = getVisualPages();
  211 + final extraObservers = navigatorObservers;
39 return Navigator( 212 return Navigator(
40 key: navigatorKey, 213 key: navigatorKey,
41 - onPopPage: _onPopPage, 214 + onPopPage: _onPopVisualRoute,
42 pages: pages, 215 pages: pages,
43 observers: [ 216 observers: [
44 GetObserver(), 217 GetObserver(),
  218 + if (extraObservers != null) ...extraObservers,
45 ], 219 ],
46 transitionDelegate: 220 transitionDelegate:
47 transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(), 221 transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
48 ); 222 );
49 } 223 }
50 224
51 - final _resultCompleter = <GetPage, Completer<Object?>>{};  
52 -  
53 @override 225 @override
54 - Future<void> setInitialRoutePath(GetPage configuration) async {  
55 - await pushRoute(configuration); 226 + Future<void> setInitialRoutePath(GetNavConfig configuration) async {
  227 + history.clear();
  228 + pageRoutes.clear();
  229 + _resultCompleter.clear();
  230 + await pushHistory(configuration);
56 } 231 }
57 232
58 @override 233 @override
59 - Future<void> setNewRoutePath(GetPage configuration) {  
60 - /// incorrect, remove routes until you reach the page in configuration.  
61 - /// if it's not found push it.  
62 - routes.clear();  
63 - pageRoutes.clear();  
64 - return pushRoute(configuration); 234 + Future<void> setNewRoutePath(GetNavConfig configuration) async {
  235 + await pushHistory(configuration);
65 } 236 }
66 237
67 - /// Called by the [Router] when it detects a route information may have  
68 - /// changed as a result of rebuild.  
69 @override 238 @override
70 - GetPage get currentConfiguration {  
71 - final route = routes.last; 239 + GetNavConfig? get currentConfiguration {
  240 + if (history.isEmpty) return null;
  241 + final route = history.last;
72 return route; 242 return route;
73 } 243 }
74 244
75 - GetPageRoute? get currentRoute => pageRoutes[currentConfiguration];  
76 -  
77 - Future<T?> toNamed<T>(String route) {  
78 - final page = Get.routeTree.matchRoute(route);  
79 - if (page.route != null) {  
80 - return pushRoute(page.route!.copy(name: route));  
81 - } else {  
82 - return pushRoute(_notFound()); 245 + GetPageRoute? get currentRoute {
  246 + final curPage = currentConfiguration?.currentPage;
  247 + return curPage == null ? null : pageRoutes[curPage];
83 } 248 }
  249 +
  250 + Future<T?> toNamed<T>(String fullRoute) {
  251 + final decoder = Get.routeTree.matchRoute(fullRoute);
  252 + return pushHistory<T>(
  253 + GetNavConfig(
  254 + currentTreeBranch: decoder.treeBranch,
  255 + location: fullRoute,
  256 + state: null, //TODO: persist state?
  257 + ),
  258 + );
84 } 259 }
85 260
86 - Future<T?> offUntil<T>(String route) {  
87 - final page = Get.routeTree.matchRoute(route);  
88 - if (page.route != null) {  
89 - return pushRoute(page.route!.copy(name: route), removeUntil: true);  
90 - } else {  
91 - return pushRoute(_notFound()); 261 + /// Removes routes according to [PopMode]
  262 + /// until it reaches the specifc [fullRoute],
  263 + /// DOES NOT remove the [fullRoute]
  264 + void until(
  265 + String fullRoute, {
  266 + PopMode popMode = PopMode.History,
  267 + }) {
  268 + // remove history or page entries until you meet route
  269 + final currentEntry = currentConfiguration;
  270 + var iterator = currentEntry;
  271 + while (history.length > 0 &&
  272 + iterator != null &&
  273 + iterator.location != fullRoute) {
  274 + _pop(popMode);
  275 + // replace iterator
  276 + iterator = currentConfiguration;
92 } 277 }
  278 + refresh();
93 } 279 }
94 280
95 GetPage _notFound() { 281 GetPage _notFound() {
@@ -101,33 +287,6 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -101,33 +287,6 @@ class GetDelegate extends RouterDelegate<GetPage>
101 ); 287 );
102 } 288 }
103 289
104 - Future<T?> pushRoute<T>(  
105 - GetPage page, {  
106 - bool removeUntil = false,  
107 - bool replaceCurrent = false,  
108 - bool rebuildStack = true,  
109 - }) {  
110 - final completer = Completer<T?>();  
111 - _resultCompleter[page] = completer;  
112 -  
113 - page = page.copy(unknownRoute: _notFound());  
114 - assert(!(removeUntil && replaceCurrent),  
115 - 'Only removeUntil or replaceCurrent should by true!');  
116 - if (removeUntil) {  
117 - routes.clear();  
118 - pageRoutes.clear();  
119 - } else if (replaceCurrent && routes.isNotEmpty) {  
120 - final lastPage = routes.removeLast();  
121 - pageRoutes.remove(lastPage);  
122 - }  
123 - addPage(page);  
124 - if (rebuildStack) {  
125 - refresh();  
126 - }  
127 - //emulate the old push with result  
128 - return completer.future;  
129 - }  
130 -  
131 Future<bool> handlePopupRoutes({ 290 Future<bool> handlePopupRoutes({
132 Object? result, 291 Object? result,
133 }) async { 292 }) async {
@@ -145,79 +304,38 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -145,79 +304,38 @@ class GetDelegate extends RouterDelegate<GetPage>
145 @override 304 @override
146 Future<bool> popRoute({ 305 Future<bool> popRoute({
147 Object? result, 306 Object? result,
  307 + PopMode popMode = PopMode.History,
148 }) async { 308 }) async {
  309 + //Returning false will cause the entire app to be popped.
149 final wasPopup = await handlePopupRoutes(result: result); 310 final wasPopup = await handlePopupRoutes(result: result);
150 if (wasPopup) return true; 311 if (wasPopup) return true;
151 -  
152 - if (canPop()) { 312 + final _popped = _pop(popMode);
  313 + refresh();
  314 + if (_popped != null) {
153 //emulate the old pop with result 315 //emulate the old pop with result
154 - final lastRoute = routes.last;  
155 - final lastCompleter = _resultCompleter.remove(lastRoute); 316 + final lastCompleter = _resultCompleter.remove(_popped);
156 lastCompleter?.complete(result); 317 lastCompleter?.complete(result);
157 - //route to be removed  
158 - removePage(lastRoute);  
159 return Future.value(true); 318 return Future.value(true);
160 } 319 }
161 return Future.value(false); 320 return Future.value(false);
162 } 321 }
163 322
164 - bool canPop() {  
165 - return routes.length > 1;  
166 - }  
167 -  
168 - bool _onPopPage(Route<dynamic> route, dynamic result) { 323 + bool _onPopVisualRoute(Route<dynamic> route, dynamic result) {
169 final didPop = route.didPop(result); 324 final didPop = route.didPop(result);
170 if (!didPop) { 325 if (!didPop) {
171 return false; 326 return false;
172 } 327 }
173 final settings = route.settings; 328 final settings = route.settings;
174 if (settings is GetPage) { 329 if (settings is GetPage) {
175 - removePage(settings);  
176 - }  
177 - refresh();  
178 - return true;  
179 - }  
180 -  
181 - void removePage(GetPage page) {  
182 - final isLast = routes.last == page;  
183 - //check if it's last  
184 - routes.remove(page);  
185 - final oldPageRoute = pageRoutes.remove(page);  
186 - if (isLast && oldPageRoute != null) {  
187 - _currentRoutePopped(oldPageRoute);  
188 - final newPageRoute = pageRoutes[routes.last];  
189 - if (newPageRoute != null) _currentRouteChanged(newPageRoute);  
190 - }  
191 - refresh();  
192 - }  
193 -  
194 - void addPage(GetPage route) {  
195 - routes.add(  
196 - route, 330 + final config = history.cast<GetNavConfig?>().firstWhere(
  331 + (element) => element?.currentPage == settings,
  332 + orElse: () => null,
197 ); 333 );
198 - final pageRoute =  
199 - pageRoutes[route] = PageRedirect(route, _notFound()).page();  
200 - _currentRouteChanged(pageRoute);  
201 - refresh(); 334 + if (config != null) {
  335 + _removeHistoryEntry(config);
202 } 336 }
203 -  
204 - void addRoutes(List<GetPage> pages) {  
205 - routes.addAll(pages);  
206 - for (var item in pages) {  
207 - pageRoutes[item] = PageRedirect(item, _notFound()).page();  
208 } 337 }
209 - final pageRoute = pageRoutes[routes.last];  
210 - if (pageRoute != null) _currentRouteChanged(pageRoute);  
211 refresh(); 338 refresh();
212 - }  
213 -  
214 - void _currentRoutePopped(GetPageRoute route) {  
215 - route.dispose();  
216 - }  
217 -  
218 - void _currentRouteChanged(GetPageRoute route) {  
219 - //is this method useful ?  
220 - //transition? -> in router outlet ??  
221 - //buildPage? -> in router outlet 339 + return true;
222 } 340 }
223 } 341 }
@@ -20,16 +20,21 @@ class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object> @@ -20,16 +20,21 @@ class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object>
20 20
21 RouterOutlet({ 21 RouterOutlet({
22 TDelegate? delegate, 22 TDelegate? delegate,
23 - required List<T> Function(TDelegate routerDelegate) currentNavStack,  
24 - required List<T> Function(List<T> currentNavStack) pickPages,  
25 - required Widget Function(BuildContext context, TDelegate, T? page) 23 + required List<RouteSettings> Function(T currentNavStack) pickPages,
  24 + required Widget Function(
  25 + BuildContext context,
  26 + TDelegate,
  27 + RouteSettings? page,
  28 + )
26 pageBuilder, 29 pageBuilder,
27 }) : this.builder( 30 }) : this.builder(
28 builder: (context, rDelegate, currentConfig) { 31 builder: (context, rDelegate, currentConfig) {
29 - final currentStack = currentNavStack(rDelegate);  
30 - final picked = pickPages(currentStack);  
31 - if (picked.length == 0) 32 + final picked = currentConfig == null
  33 + ? <RouteSettings>[]
  34 + : pickPages(currentConfig);
  35 + if (picked.length == 0) {
32 return pageBuilder(context, rDelegate, null); 36 return pageBuilder(context, rDelegate, null);
  37 + }
33 return pageBuilder(context, rDelegate, picked.last); 38 return pageBuilder(context, rDelegate, picked.last);
34 }, 39 },
35 delegate: delegate, 40 delegate: delegate,
@@ -68,12 +73,12 @@ class _RouterOutletState<TDelegate extends RouterDelegate<T>, T extends Object> @@ -68,12 +73,12 @@ class _RouterOutletState<TDelegate extends RouterDelegate<T>, T extends Object>
68 } 73 }
69 } 74 }
70 75
71 -class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> { 76 +class GetRouterOutlet extends RouterOutlet<GetDelegate, GetNavConfig> {
72 GetRouterOutlet.builder({ 77 GetRouterOutlet.builder({
73 required Widget Function( 78 required Widget Function(
74 BuildContext context, 79 BuildContext context,
75 GetDelegate delegate, 80 GetDelegate delegate,
76 - GetPage? currentRoute, 81 + GetNavConfig? currentRoute,
77 ) 82 )
78 builder, 83 builder,
79 GetDelegate? routerDelegate, 84 GetDelegate? routerDelegate,
@@ -84,10 +89,10 @@ class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> { @@ -84,10 +89,10 @@ class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> {
84 89
85 GetRouterOutlet({ 90 GetRouterOutlet({
86 Widget Function(GetDelegate delegate)? emptyPage, 91 Widget Function(GetDelegate delegate)? emptyPage,
87 - required List<GetPage> Function(List<GetPage> currentNavStack) pickPages, 92 + required List<GetPage> Function(GetNavConfig currentNavStack) pickPages,
88 }) : super( 93 }) : super(
89 pageBuilder: (context, rDelegate, page) { 94 pageBuilder: (context, rDelegate, page) {
90 - final pageRoute = rDelegate.pageRoutes[page]; 95 + final pageRoute = rDelegate.pageRoutes[page?.name];
91 if (pageRoute != null) { 96 if (pageRoute != null) {
92 //TODO: transitions go here ! 97 //TODO: transitions go here !
93 return pageRoute.buildPage( 98 return pageRoute.buildPage(
@@ -102,21 +107,18 @@ class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> { @@ -102,21 +107,18 @@ class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> {
102 rDelegate.notFoundRoute?.page()) ?? 107 rDelegate.notFoundRoute?.page()) ??
103 SizedBox.shrink(); 108 SizedBox.shrink();
104 }, 109 },
105 - currentNavStack: (routerDelegate) => routerDelegate.routes,  
106 pickPages: pickPages, 110 pickPages: pickPages,
107 delegate: Get.getDelegate(), 111 delegate: Get.getDelegate(),
108 ); 112 );
109 } 113 }
110 114
  115 +/// A marker outlet to identify which pages are visual
  116 +/// (handled by the navigator) and which are logical
  117 +/// (handled by the delegate)
111 class RouterOutletContainerMiddleWare extends GetMiddleware { 118 class RouterOutletContainerMiddleWare extends GetMiddleware {
112 final String stayAt; 119 final String stayAt;
113 120
114 RouterOutletContainerMiddleWare(this.stayAt); 121 RouterOutletContainerMiddleWare(this.stayAt);
115 - // @override  
116 - // RouteSettings? redirect(String? route) {  
117 - // print('RouterOutletContainerMiddleWare: Redirect called ($route)');  
118 - // return null;  
119 - // }  
120 } 122 }
121 123
122 extension PagesListExt on List<GetPage> { 124 extension PagesListExt on List<GetPage> {
1 -import '../../../get_core/src/get_main.dart';  
2 import '../../get_navigation.dart'; 1 import '../../get_navigation.dart';
3 import '../routes/get_route.dart'; 2 import '../routes/get_route.dart';
4 3
5 class RouteDecoder { 4 class RouteDecoder {
6 - final GetPage? route;  
7 - final Map<String, String?> parameters;  
8 - const RouteDecoder(this.route, this.parameters); 5 + final List<GetPage> treeBranch;
  6 + GetPage? get route => treeBranch.isEmpty ? null : treeBranch.last;
  7 + final Map<String, String> parameters;
  8 + const RouteDecoder(
  9 + this.treeBranch,
  10 + this.parameters,
  11 + );
9 } 12 }
10 13
11 class ParseRouteTree { 14 class ParseRouteTree {
@@ -17,20 +20,61 @@ class ParseRouteTree { @@ -17,20 +20,61 @@ class ParseRouteTree {
17 20
18 RouteDecoder matchRoute(String name) { 21 RouteDecoder matchRoute(String name) {
19 final uri = Uri.parse(name); 22 final uri = Uri.parse(name);
20 - final route = _findRoute(uri.path);  
21 - final params = Map<String, String?>.from(uri.queryParameters);  
22 - if (route != null) {  
23 - final parsedParams = _parseParams(name, route.path); 23 + // /home/profile/123 => home,profile,123 => /,/home,/home/profile,/home/profile/123
  24 + final split = uri.path.split('/').where((element) => element.isNotEmpty);
  25 + var curPath = '/';
  26 + final cumulativePaths = <String>[
  27 + '/',
  28 + ];
  29 + for (var item in split) {
  30 + if (curPath.endsWith('/')) {
  31 + curPath += '$item';
  32 + } else {
  33 + curPath += '/$item';
  34 + }
  35 + cumulativePaths.add(curPath);
  36 + }
  37 +
  38 + final treeBranch = cumulativePaths
  39 + .map(
  40 + (p) {
  41 + final res = _findRoute(p);
  42 + //change GetPage name from the regex to the actual name
  43 + return res?.copy(
  44 + name: p,
  45 + );
  46 + },
  47 + )
  48 + .where((element) => element != null)
  49 + .cast<GetPage>()
  50 + .toList();
  51 + final params = Map<String, String>.from(uri.queryParameters);
  52 + if (treeBranch.isNotEmpty) {
  53 + //route is found, do further parsing to get nested query params
  54 + final lastRoute = treeBranch.last;
  55 + final parsedParams = _parseParams(name, lastRoute.path);
24 if (parsedParams.isNotEmpty) { 56 if (parsedParams.isNotEmpty) {
25 params.addAll(parsedParams); 57 params.addAll(parsedParams);
26 } 58 }
  59 + //copy parameters to all pages.
  60 + final mappedTreeBranch = treeBranch
  61 + .map(
  62 + (e) => e.copy(
  63 + parameter: params,
  64 + ),
  65 + )
  66 + .toList();
  67 + return RouteDecoder(
  68 + mappedTreeBranch,
  69 + params,
  70 + );
27 } 71 }
28 - // This logger sends confusing messages  
29 - // else {  
30 - // // Get.log('Route "${uri.path}" not found');  
31 - // }  
32 72
33 - return RouteDecoder(route, params); 73 + //route not found
  74 + return RouteDecoder(
  75 + treeBranch,
  76 + params,
  77 + );
34 } 78 }
35 79
36 void addRoutes(List<GetPage> getPages) { 80 void addRoutes(List<GetPage> getPages) {
@@ -99,8 +143,8 @@ class ParseRouteTree { @@ -99,8 +143,8 @@ class ParseRouteTree {
99 ); 143 );
100 } 144 }
101 145
102 - Map<String, String?> _parseParams(String path, PathDecoded routePath) {  
103 - final params = <String, String?>{}; 146 + Map<String, String> _parseParams(String path, PathDecoded routePath) {
  147 + final params = <String, String>{};
104 var idx = path.indexOf('?'); 148 var idx = path.indexOf('?');
105 if (idx > -1) { 149 if (idx > -1) {
106 path = path.substring(0, idx); 150 path = path.substring(0, idx);
@@ -41,7 +41,7 @@ class GetPage<T> extends Page<T> { @@ -41,7 +41,7 @@ class GetPage<T> extends Page<T> {
41 final CustomTransition? customTransition; 41 final CustomTransition? customTransition;
42 final Duration? transitionDuration; 42 final Duration? transitionDuration;
43 final bool fullscreenDialog; 43 final bool fullscreenDialog;
44 - 44 + final bool preventDuplicates;
45 // @override 45 // @override
46 // final LocalKey? key; 46 // final LocalKey? key;
47 47
@@ -79,6 +79,7 @@ class GetPage<T> extends Page<T> { @@ -79,6 +79,7 @@ class GetPage<T> extends Page<T> {
79 this.children, 79 this.children,
80 this.middlewares, 80 this.middlewares,
81 this.unknownRoute, 81 this.unknownRoute,
  82 + this.preventDuplicates = false,
82 }) : path = _nameToRegex(name), 83 }) : path = _nameToRegex(name),
83 super( 84 super(
84 key: ValueKey(name), 85 key: ValueKey(name),
@@ -128,8 +129,10 @@ class GetPage<T> extends Page<T> { @@ -128,8 +129,10 @@ class GetPage<T> extends Page<T> {
128 List<GetPage>? children, 129 List<GetPage>? children,
129 GetPage? unknownRoute, 130 GetPage? unknownRoute,
130 List<GetMiddleware>? middlewares, 131 List<GetMiddleware>? middlewares,
  132 + bool? preventDuplicates,
131 }) { 133 }) {
132 return GetPage( 134 return GetPage(
  135 + preventDuplicates: preventDuplicates ?? this.preventDuplicates,
133 name: name ?? this.name, 136 name: name ?? this.name,
134 page: page ?? this.page, 137 page: page ?? this.page,
135 popGesture: popGesture ?? this.popGesture, 138 popGesture: popGesture ?? this.popGesture,
@@ -145,7 +148,6 @@ class GetPage<T> extends Page<T> { @@ -145,7 +148,6 @@ class GetPage<T> extends Page<T> {
145 customTransition: customTransition ?? this.customTransition, 148 customTransition: customTransition ?? this.customTransition,
146 transitionDuration: transitionDuration ?? this.transitionDuration, 149 transitionDuration: transitionDuration ?? this.transitionDuration,
147 fullscreenDialog: fullscreenDialog ?? this.fullscreenDialog, 150 fullscreenDialog: fullscreenDialog ?? this.fullscreenDialog,
148 - // settings: settings ?? this.settings,  
149 children: children ?? this.children, 151 children: children ?? this.children,
150 unknownRoute: unknownRoute ?? this.unknownRoute, 152 unknownRoute: unknownRoute ?? this.unknownRoute,
151 middlewares: middlewares ?? this.middlewares, 153 middlewares: middlewares ?? this.middlewares,
@@ -171,6 +173,8 @@ class GetPage<T> extends Page<T> { @@ -171,6 +173,8 @@ class GetPage<T> extends Page<T> {
171 other.page.runtimeType == page.runtimeType && 173 other.page.runtimeType == page.runtimeType &&
172 other.popGesture == popGesture && 174 other.popGesture == popGesture &&
173 // mapEquals(other.parameter, parameter) && 175 // mapEquals(other.parameter, parameter) &&
  176 +
  177 + other.preventDuplicates == preventDuplicates &&
174 other.title == title && 178 other.title == title &&
175 other.transition == transition && 179 other.transition == transition &&
176 other.curve == curve && 180 other.curve == curve &&
@@ -194,6 +198,7 @@ class GetPage<T> extends Page<T> { @@ -194,6 +198,7 @@ class GetPage<T> extends Page<T> {
194 return //page.hashCode ^ 198 return //page.hashCode ^
195 popGesture.hashCode ^ 199 popGesture.hashCode ^
196 // parameter.hashCode ^ 200 // parameter.hashCode ^
  201 + preventDuplicates.hashCode ^
197 title.hashCode ^ 202 title.hashCode ^
198 transition.hashCode ^ 203 transition.hashCode ^
199 curve.hashCode ^ 204 curve.hashCode ^