Jonny Borges
Committed by GitHub

Merge pull request #1511 from Bdaya-Dev/router-outlet

More Improvements to the new RouterOutlet
@@ -20,6 +20,13 @@ @@ -20,6 +20,13 @@
20 "cwd": "example_nav2", 20 "cwd": "example_nav2",
21 "request": "launch", 21 "request": "launch",
22 "type": "dart" 22 "type": "dart"
  23 + },
  24 + {
  25 + "name": "example_nav2 WEB",
  26 + "cwd": "example_nav2",
  27 + "request": "launch",
  28 + "type": "dart",
  29 + "deviceId": "Chrome"
23 } 30 }
24 ] 31 ]
25 } 32 }
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,41 +22,49 @@ class AppPages { @@ -20,41 +22,49 @@ class AppPages {
20 22
21 static final routes = [ 23 static final routes = [
22 GetPage( 24 GetPage(
23 - name: _Paths.HOME,  
24 - page: () => HomeView(),  
25 - bindings: [  
26 - HomeBinding(),  
27 - ],  
28 - title: null, 25 + name: '/',
  26 + page: () => RootView(),
29 middlewares: [ 27 middlewares: [
30 - RouterOutletContainerMiddleWare(_Paths.HOME), 28 + RouterOutletContainerMiddleWare('/'),
31 ], 29 ],
  30 + binding: RootBinding(),
32 children: [ 31 children: [
33 GetPage( 32 GetPage(
34 - name: _Paths.PROFILE,  
35 - page: () => ProfileView(),  
36 - title: 'Profile',  
37 - binding: ProfileBinding(),  
38 - ),  
39 - GetPage(  
40 - name: _Paths.PRODUCTS,  
41 - page: () => ProductsView(),  
42 - title: 'Products',  
43 - binding: ProductsBinding(), 33 + name: _Paths.HOME,
  34 + preventDuplicates: true,
  35 + page: () => HomeView(),
  36 + bindings: [
  37 + HomeBinding(),
  38 + ],
  39 + title: null,
44 children: [ 40 children: [
45 GetPage( 41 GetPage(
46 - name: _Paths.PRODUCT_DETAILS,  
47 - page: () => ProductDetailsView(),  
48 - binding: ProductDetailsBinding(), 42 + name: _Paths.PROFILE,
  43 + page: () => ProfileView(),
  44 + title: 'Profile',
  45 + binding: ProfileBinding(),
  46 + ),
  47 + GetPage(
  48 + name: _Paths.PRODUCTS,
  49 + page: () => ProductsView(),
  50 + title: 'Products',
  51 + binding: ProductsBinding(),
  52 + children: [
  53 + GetPage(
  54 + name: _Paths.PRODUCT_DETAILS,
  55 + page: () => ProductDetailsView(),
  56 + binding: ProductDetailsBinding(),
  57 + ),
  58 + ],
49 ), 59 ),
50 ], 60 ],
51 ), 61 ),
  62 + GetPage(
  63 + name: _Paths.SETTINGS,
  64 + page: () => SettingsView(),
  65 + binding: SettingsBinding(),
  66 + ),
52 ], 67 ],
53 ), 68 ),
54 - GetPage(  
55 - name: _Paths.SETTINGS,  
56 - page: () => SettingsView(),  
57 - binding: SettingsBinding(),  
58 - ),  
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 + }
  182 +
  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 + }
24 192
25 - List<GetPage> getVisiblePages() {  
26 - return routes.where((r) { 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,63 +205,77 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -31,63 +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 - routes.clear();  
61 - pageRoutes.clear();  
62 - return pushRoute(configuration); 234 + Future<void> setNewRoutePath(GetNavConfig configuration) async {
  235 + await pushHistory(configuration);
63 } 236 }
64 237
65 - /// Called by the [Router] when it detects a route information may have  
66 - /// changed as a result of rebuild.  
67 @override 238 @override
68 - GetPage get currentConfiguration {  
69 - final route = routes.last; 239 + GetNavConfig? get currentConfiguration {
  240 + if (history.isEmpty) return null;
  241 + final route = history.last;
70 return route; 242 return route;
71 } 243 }
72 244
73 - GetPageRoute? get currentRoute => pageRoutes[currentConfiguration]; 245 + GetPageRoute? get currentRoute {
  246 + final curPage = currentConfiguration?.currentPage;
  247 + return curPage == null ? null : pageRoutes[curPage];
  248 + }
74 249
75 - Future<T?> toNamed<T>(String route) {  
76 - final page = Get.routeTree.matchRoute(route);  
77 - if (page.route != null) {  
78 - return pushRoute(page.route!.copy(name: route));  
79 - } else {  
80 - return pushRoute(_notFound());  
81 - } 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 + );
82 } 259 }
83 260
84 - Future<T?> offUntil<T>(String route) {  
85 - final page = Get.routeTree.matchRoute(route);  
86 - if (page.route != null) {  
87 - return pushRoute(page.route!.copy(name: route), removeUntil: true);  
88 - } else {  
89 - 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;
90 } 277 }
  278 + refresh();
91 } 279 }
92 280
93 GetPage _notFound() { 281 GetPage _notFound() {
@@ -99,33 +287,6 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -99,33 +287,6 @@ class GetDelegate extends RouterDelegate<GetPage>
99 ); 287 );
100 } 288 }
101 289
102 - Future<T?> pushRoute<T>(  
103 - GetPage page, {  
104 - bool removeUntil = false,  
105 - bool replaceCurrent = false,  
106 - bool rebuildStack = true,  
107 - }) {  
108 - final completer = Completer<T?>();  
109 - _resultCompleter[page] = completer;  
110 -  
111 - page = page.copy(unknownRoute: _notFound());  
112 - assert(!(removeUntil && replaceCurrent),  
113 - 'Only removeUntil or replaceCurrent should by true!');  
114 - if (removeUntil) {  
115 - routes.clear();  
116 - pageRoutes.clear();  
117 - } else if (replaceCurrent && routes.isNotEmpty) {  
118 - final lastPage = routes.removeLast();  
119 - pageRoutes.remove(lastPage);  
120 - }  
121 - addPage(page);  
122 - if (rebuildStack) {  
123 - refresh();  
124 - }  
125 - //emulate the old push with result  
126 - return completer.future;  
127 - }  
128 -  
129 Future<bool> handlePopupRoutes({ 290 Future<bool> handlePopupRoutes({
130 Object? result, 291 Object? result,
131 }) async { 292 }) async {
@@ -143,79 +304,38 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -143,79 +304,38 @@ class GetDelegate extends RouterDelegate<GetPage>
143 @override 304 @override
144 Future<bool> popRoute({ 305 Future<bool> popRoute({
145 Object? result, 306 Object? result,
  307 + PopMode popMode = PopMode.History,
146 }) async { 308 }) async {
  309 + //Returning false will cause the entire app to be popped.
147 final wasPopup = await handlePopupRoutes(result: result); 310 final wasPopup = await handlePopupRoutes(result: result);
148 if (wasPopup) return true; 311 if (wasPopup) return true;
149 -  
150 - if (canPop()) { 312 + final _popped = _pop(popMode);
  313 + refresh();
  314 + if (_popped != null) {
151 //emulate the old pop with result 315 //emulate the old pop with result
152 - final lastRoute = routes.last;  
153 - final lastCompleter = _resultCompleter.remove(lastRoute); 316 + final lastCompleter = _resultCompleter.remove(_popped);
154 lastCompleter?.complete(result); 317 lastCompleter?.complete(result);
155 - //route to be removed  
156 - removePage(lastRoute);  
157 return Future.value(true); 318 return Future.value(true);
158 } 319 }
159 return Future.value(false); 320 return Future.value(false);
160 } 321 }
161 322
162 - bool canPop() {  
163 - return routes.length > 1;  
164 - }  
165 -  
166 - bool _onPopPage(Route<dynamic> route, dynamic result) { 323 + bool _onPopVisualRoute(Route<dynamic> route, dynamic result) {
167 final didPop = route.didPop(result); 324 final didPop = route.didPop(result);
168 if (!didPop) { 325 if (!didPop) {
169 return false; 326 return false;
170 } 327 }
171 final settings = route.settings; 328 final settings = route.settings;
172 if (settings is GetPage) { 329 if (settings is GetPage) {
173 - removePage(settings); 330 + final config = history.cast<GetNavConfig?>().firstWhere(
  331 + (element) => element?.currentPage == settings,
  332 + orElse: () => null,
  333 + );
  334 + if (config != null) {
  335 + _removeHistoryEntry(config);
  336 + }
174 } 337 }
175 refresh(); 338 refresh();
176 return true; 339 return true;
177 } 340 }
178 -  
179 - void removePage(GetPage page) {  
180 - final isLast = routes.last == page;  
181 - //check if it's last  
182 - routes.remove(page);  
183 - final oldPageRoute = pageRoutes.remove(page);  
184 - if (isLast && oldPageRoute != null) {  
185 - _currentRoutePopped(oldPageRoute);  
186 - final newPageRoute = pageRoutes[routes.last];  
187 - if (newPageRoute != null) _currentRouteChanged(newPageRoute);  
188 - }  
189 - refresh();  
190 - }  
191 -  
192 - void addPage(GetPage route) {  
193 - routes.add(  
194 - route,  
195 - );  
196 - final pageRoute =  
197 - pageRoutes[route] = PageRedirect(route, _notFound()).page();  
198 - _currentRouteChanged(pageRoute);  
199 - refresh();  
200 - }  
201 -  
202 - void addRoutes(List<GetPage> pages) {  
203 - routes.addAll(pages);  
204 - for (var item in pages) {  
205 - pageRoutes[item] = PageRedirect(item, _notFound()).page();  
206 - }  
207 - final pageRoute = pageRoutes[routes.last];  
208 - if (pageRoute != null) _currentRouteChanged(pageRoute);  
209 - refresh();  
210 - }  
211 -  
212 - void _currentRoutePopped(GetPageRoute route) {  
213 - route.dispose();  
214 - }  
215 -  
216 - void _currentRouteChanged(GetPageRoute route) {  
217 - //is this method useful ?  
218 - //transition? -> in router outlet ??  
219 - //buildPage? -> in router outlet  
220 - }  
221 } 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,53 @@ class ParseRouteTree { @@ -17,20 +20,53 @@ 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((e) => MapEntry(e, _findRoute(e)))
  40 + .where((element) => element.value != null)
  41 + .toList();
  42 +
  43 + final params = Map<String, String>.from(uri.queryParameters);
  44 + if (treeBranch.isNotEmpty) {
  45 + //route is found, do further parsing to get nested query params
  46 + final lastRoute = treeBranch.last;
  47 + final parsedParams = _parseParams(name, lastRoute.value!.path);
24 if (parsedParams.isNotEmpty) { 48 if (parsedParams.isNotEmpty) {
25 params.addAll(parsedParams); 49 params.addAll(parsedParams);
26 } 50 }
  51 + //copy parameters to all pages.
  52 + final mappedTreeBranch = treeBranch
  53 + .map(
  54 + (e) => e.value!.copy(
  55 + parameter: params,
  56 + ),
  57 + )
  58 + .toList();
  59 + return RouteDecoder(
  60 + mappedTreeBranch,
  61 + params,
  62 + );
27 } 63 }
28 - // This logger sends confusing messages  
29 - // else {  
30 - // // Get.log('Route "${uri.path}" not found');  
31 - // }  
32 64
33 - return RouteDecoder(route, params); 65 + //route not found
  66 + return RouteDecoder(
  67 + treeBranch.map((e) => e.value!).toList(),
  68 + params,
  69 + );
34 } 70 }
35 71
36 void addRoutes(List<GetPage> getPages) { 72 void addRoutes(List<GetPage> getPages) {
@@ -88,6 +124,7 @@ class ParseRouteTree { @@ -88,6 +124,7 @@ class ParseRouteTree {
88 opaque: origin.opaque, 124 opaque: origin.opaque,
89 parameter: origin.parameter, 125 parameter: origin.parameter,
90 popGesture: origin.popGesture, 126 popGesture: origin.popGesture,
  127 +
91 // settings: origin.settings, 128 // settings: origin.settings,
92 transitionDuration: origin.transitionDuration, 129 transitionDuration: origin.transitionDuration,
93 middlewares: middlewares, 130 middlewares: middlewares,
@@ -99,8 +136,8 @@ class ParseRouteTree { @@ -99,8 +136,8 @@ class ParseRouteTree {
99 ); 136 );
100 } 137 }
101 138
102 - Map<String, String?> _parseParams(String path, PathDecoded routePath) {  
103 - final params = <String, String?>{}; 139 + Map<String, String> _parseParams(String path, PathDecoded routePath) {
  140 + final params = <String, String>{};
104 var idx = path.indexOf('?'); 141 var idx = path.indexOf('?');
105 if (idx > -1) { 142 if (idx > -1) {
106 path = path.substring(0, idx); 143 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 ^