Ahmed Fwela

WIP

successfully decoupled the logical routes from the visual routes
  1 +class DemoProduct {
  2 + final String name;
  3 + final String id;
  4 +
  5 + DemoProduct({
  6 + required this.name,
  7 + required this.id,
  8 + });
  9 +}
  1 +import 'package:example_nav2/app/modules/home/controllers/home_controller.dart';
  2 +import 'package:flutter/material.dart';
  3 +
  4 +import 'package:get/get.dart';
  5 +
  6 +class DashboardView extends GetView<HomeController> {
  7 + @override
  8 + Widget build(BuildContext context) {
  9 + return Scaffold(
  10 + body: Center(
  11 + child: Text(
  12 + 'DashboardView is working',
  13 + style: TextStyle(fontSize: 20),
  14 + ),
  15 + ),
  16 + );
  17 + }
  18 +}
  1 +import 'package:example_nav2/app/modules/home/views/dashboard_view.dart';
  2 +import 'package:example_nav2/app/routes/app_pages.dart';
1 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
2 4
3 import 'package:get/get.dart'; 5 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';
4 8
5 import '../controllers/home_controller.dart'; 9 import '../controllers/home_controller.dart';
6 10
7 class HomeView extends GetView<HomeController> { 11 class HomeView extends GetView<HomeController> {
8 @override 12 @override
9 Widget build(BuildContext context) { 13 Widget build(BuildContext context) {
10 - return Scaffold(  
11 - appBar: AppBar(  
12 - title: Text('HomeView'),  
13 - centerTitle: true,  
14 - ),  
15 - body: Center(  
16 - child: Text(  
17 - 'HomeView is working',  
18 - style: TextStyle(fontSize: 20),  
19 - ),  
20 - ), 14 + return GetRouterOutlet.builder(
  15 + builder: (context, delegate, currentRoute) {
  16 + final title = currentRoute?.title;
  17 + final currentName = currentRoute?.name;
  18 + var currentIndex = 0;
  19 + if (currentName?.startsWith(Routes.PRODUCTS) == true) currentIndex = 2;
  20 + if (currentName?.startsWith(Routes.PROFILE) == true) currentIndex = 1;
  21 + return Scaffold(
  22 + appBar: title == null
  23 + ? null
  24 + : AppBar(
  25 + title: Text(title),
  26 + centerTitle: true,
  27 + ),
  28 + body: GetRouterOutlet(
  29 + emptyStackPage: (delegate) => DashboardView(),
  30 + pickPages: (currentNavStack) {
  31 + // will take any route after home
  32 + final res = currentNavStack.pickAfterRoute(Routes.HOME);
  33 + print('''RouterOutlet rebuild:
  34 + currentStack: $currentNavStack
  35 + pickedStack: $res''');
  36 + return res;
  37 + },
  38 + ),
  39 + bottomNavigationBar: BottomNavigationBar(
  40 + currentIndex: currentIndex,
  41 + onTap: (value) {
  42 + final getDelegate = Get.getDelegate();
  43 + if (getDelegate == null) return;
  44 + switch (value) {
  45 + case 0:
  46 + getDelegate.offUntil(Routes.HOME);
  47 + break;
  48 + case 1:
  49 + getDelegate.toNamed(Routes.PROFILE);
  50 + break;
  51 + case 2:
  52 + getDelegate.toNamed(Routes.PRODUCTS);
  53 + break;
  54 + default:
  55 + }
  56 + },
  57 + items: [
  58 + // Routes.Home + [Empty]
  59 + BottomNavigationBarItem(
  60 + icon: Icon(Icons.home),
  61 + label: 'Home',
  62 + ),
  63 + // Routes.Home + Routes.Profile
  64 + BottomNavigationBarItem(
  65 + icon: Icon(Icons.account_box_rounded),
  66 + label: 'Profile',
  67 + ),
  68 + // Routes.Home + Routes.Products
  69 + BottomNavigationBarItem(
  70 + icon: Icon(Icons.account_box_rounded),
  71 + label: 'Products',
  72 + ),
  73 + ],
  74 + ),
  75 + );
  76 + },
21 ); 77 );
22 } 78 }
23 } 79 }
@@ -5,8 +5,10 @@ import '../controllers/product_details_controller.dart'; @@ -5,8 +5,10 @@ import '../controllers/product_details_controller.dart';
5 class ProductDetailsBinding extends Bindings { 5 class ProductDetailsBinding extends Bindings {
6 @override 6 @override
7 void dependencies() { 7 void dependencies() {
8 - Get.lazyPut<ProductDetailsController>(  
9 - () => ProductDetailsController(), 8 + Get.create<ProductDetailsController>(
  9 + () => ProductDetailsController(
  10 + Get.parameters['productId'] ?? '',
  11 + ),
10 ); 12 );
11 } 13 }
12 } 14 }
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 2
3 -class ProductDetailsController extends GetxController {} 3 +class ProductDetailsController extends GetxController {
  4 + final String productId;
  5 +
  6 + ProductDetailsController(this.productId);
  7 +}
@@ -4,18 +4,20 @@ import 'package:get/get.dart'; @@ -4,18 +4,20 @@ import 'package:get/get.dart';
4 4
5 import '../controllers/product_details_controller.dart'; 5 import '../controllers/product_details_controller.dart';
6 6
7 -class ProductDetailsView extends GetView<ProductDetailsController> { 7 +class ProductDetailsView extends GetWidget<ProductDetailsController> {
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('ProductDetailsView'),  
13 - centerTitle: true,  
14 - ),  
15 body: Center( 11 body: Center(
16 - child: Text(  
17 - 'ProductDetailsView is working',  
18 - style: TextStyle(fontSize: 20), 12 + child: Column(
  13 + mainAxisSize: MainAxisSize.min,
  14 + children: [
  15 + Text(
  16 + 'ProductDetailsView is working',
  17 + style: TextStyle(fontSize: 20),
  18 + ),
  19 + Text('ProductId: ${controller.productId}')
  20 + ],
19 ), 21 ),
20 ), 22 ),
21 ); 23 );
  1 +import 'package:example_nav2/app/models/demo_product.dart';
1 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
2 3
3 class ProductsController extends GetxController { 4 class ProductsController extends GetxController {
4 - //TODO: Implement ProductsController 5 + final products = <DemoProduct>[].obs;
5 6
6 - final count = 0.obs;  
7 - @override  
8 - void onInit() {  
9 - super.onInit(); 7 + void loadDemoProductsFromSomeWhere() {
  8 + products.add(
  9 + DemoProduct(
  10 + name: 'Product added on: ${DateTime.now().toString()}',
  11 + id: DateTime.now().millisecondsSinceEpoch.toString(),
  12 + ),
  13 + );
10 } 14 }
11 15
12 @override 16 @override
13 void onReady() { 17 void onReady() {
14 super.onReady(); 18 super.onReady();
  19 + loadDemoProductsFromSomeWhere();
15 } 20 }
16 -  
17 - @override  
18 - void onClose() {}  
19 - void increment() => count.value++;  
20 } 21 }
  1 +import 'package:example_nav2/app/routes/app_pages.dart';
1 import 'package:flutter/material.dart'; 2 import 'package:flutter/material.dart';
2 3
3 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
@@ -8,14 +9,29 @@ class ProductsView extends GetView<ProductsController> { @@ -8,14 +9,29 @@ class ProductsView extends GetView<ProductsController> {
8 @override 9 @override
9 Widget build(BuildContext context) { 10 Widget build(BuildContext context) {
10 return Scaffold( 11 return Scaffold(
11 - appBar: AppBar(  
12 - title: Text('ProductsView'),  
13 - centerTitle: true, 12 + floatingActionButton: FloatingActionButton.extended(
  13 + onPressed: controller.loadDemoProductsFromSomeWhere,
  14 + label: Text('Add'),
14 ), 15 ),
15 - body: Center(  
16 - child: Text(  
17 - 'ProductsView is working',  
18 - style: TextStyle(fontSize: 20), 16 + body: Obx(
  17 + () => RefreshIndicator(
  18 + onRefresh: () async {
  19 + controller.products.clear();
  20 + controller.loadDemoProductsFromSomeWhere();
  21 + },
  22 + child: ListView.builder(
  23 + itemCount: controller.products.length,
  24 + itemBuilder: (context, index) {
  25 + final item = controller.products[index];
  26 + return ListTile(
  27 + onTap: () {
  28 + Get.getDelegate()?.toNamed(Routes.PRODUCT_DETAILS(item.id));
  29 + },
  30 + title: Text(item.name),
  31 + subtitle: Text(item.id),
  32 + );
  33 + },
  34 + ),
19 ), 35 ),
20 ), 36 ),
21 ); 37 );
@@ -8,10 +8,6 @@ class ProfileView extends GetView<ProfileController> { @@ -8,10 +8,6 @@ class ProfileView extends GetView<ProfileController> {
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('ProfileView'),  
13 - centerTitle: true,  
14 - ),  
15 body: Center( 11 body: Center(
16 child: Text( 12 child: Text(
17 'ProfileView is working', 13 'ProfileView is working',
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 - 2 +import 'package:get/get_navigation/src/nav2/router_outlet.dart';
3 import '../modules/home/bindings/home_binding.dart'; 3 import '../modules/home/bindings/home_binding.dart';
4 import '../modules/home/views/home_view.dart'; 4 import '../modules/home/views/home_view.dart';
5 import '../modules/product_details/bindings/product_details_binding.dart'; 5 import '../modules/product_details/bindings/product_details_binding.dart';
@@ -22,24 +22,34 @@ class AppPages { @@ -22,24 +22,34 @@ class AppPages {
22 GetPage( 22 GetPage(
23 name: _Paths.HOME, 23 name: _Paths.HOME,
24 page: () => HomeView(), 24 page: () => HomeView(),
25 - binding: HomeBinding(), 25 + //TODO: don't group bindings in one place, and instead make each page use its own binding
  26 + bindings: [
  27 + HomeBinding(),
  28 + //These must use [Get.lazyPut] or [Get.create] because their view is created long after they are declared
  29 + ProfileBinding(),
  30 + ProductsBinding(),
  31 + ProductDetailsBinding(),
  32 + ],
  33 + title: null,
  34 + middlewares: [
  35 + RouterOutletContainerMiddleWare(_Paths.HOME),
  36 + ],
26 children: [ 37 children: [
27 GetPage( 38 GetPage(
28 name: _Paths.PROFILE, 39 name: _Paths.PROFILE,
29 page: () => ProfileView(), 40 page: () => ProfileView(),
30 - binding: ProfileBinding(), 41 + title: 'Profile',
31 ), 42 ),
32 - ],  
33 - ),  
34 - GetPage(  
35 - name: _Paths.PRODUCTS,  
36 - page: () => ProductsView(),  
37 - binding: ProductsBinding(),  
38 - children: [  
39 GetPage( 43 GetPage(
40 - name: _Paths.PRODUCT_DETAILS,  
41 - page: () => ProductDetailsView(),  
42 - binding: ProductDetailsBinding(), 44 + name: _Paths.PRODUCTS,
  45 + page: () => ProductsView(),
  46 + title: 'Products',
  47 + children: [
  48 + GetPage(
  49 + name: _Paths.PRODUCT_DETAILS,
  50 + page: () => ProductDetailsView(),
  51 + ),
  52 + ],
43 ), 53 ),
44 ], 54 ],
45 ), 55 ),
@@ -5,12 +5,12 @@ abstract class Routes { @@ -5,12 +5,12 @@ abstract class Routes {
5 Routes._(); 5 Routes._();
6 6
7 static const HOME = _Paths.HOME; 7 static const HOME = _Paths.HOME;
8 - static const PROFILE = _Paths.PROFILE; 8 + static const PROFILE = _Paths.HOME + _Paths.PROFILE;
9 9
10 static const SETTINGS = _Paths.SETTINGS; 10 static const SETTINGS = _Paths.SETTINGS;
11 11
12 - static const PRODUCTS = _Paths.PRODUCTS;  
13 - static PRODUCT_DETAILS(String productId) => '${_Paths.PRODUCTS}/$productId'; 12 + static const PRODUCTS = _Paths.HOME + _Paths.PRODUCTS;
  13 + static String PRODUCT_DETAILS(String productId) => '$PRODUCTS/$productId';
14 } 14 }
15 15
16 abstract class _Paths { 16 abstract class _Paths {
1 import 'package:flutter/widgets.dart'; 1 import 'package:flutter/widgets.dart';
  2 +import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
2 3
3 import '../../get_core/src/get_interface.dart'; 4 import '../../get_core/src/get_interface.dart';
4 5
  6 +import '../../route_manager.dart';
5 import 'get_instance.dart'; 7 import 'get_instance.dart';
6 8
7 extension Inst on GetInterface { 9 extension Inst on GetInterface {
@@ -127,4 +129,6 @@ extension Inst on GetInterface { @@ -127,4 +129,6 @@ extension Inst on GetInterface {
127 /// Casts the stored router delegate to a desired type 129 /// Casts the stored router delegate to a desired type
128 TDelegate? delegate<TDelegate extends RouterDelegate<TPage>, TPage>() => 130 TDelegate? delegate<TDelegate extends RouterDelegate<TPage>, TPage>() =>
129 routerDelegate as TDelegate?; 131 routerDelegate as TDelegate?;
  132 +
  133 + GetDelegate? getDelegate() => delegate<GetDelegate, GetPage>();
130 } 134 }
1 import 'dart:async'; 1 import 'dart:async';
2 2
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
  4 +import 'package:get/get_navigation/src/nav2/router_outlet.dart';
4 import '../../../get.dart'; 5 import '../../../get.dart';
5 import '../../../get_state_manager/src/simple/list_notifier.dart'; 6 import '../../../get_state_manager/src/simple/list_notifier.dart';
6 7
@@ -8,7 +9,7 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -8,7 +9,7 @@ class GetDelegate extends RouterDelegate<GetPage>
8 with ListenableMixin, ListNotifierMixin { 9 with ListenableMixin, ListNotifierMixin {
9 final List<GetPage> routes = <GetPage>[]; 10 final List<GetPage> routes = <GetPage>[];
10 11
11 - final GetPage? notFoundRoute; 12 + GetPage? notFoundRoute;
12 13
13 final List<NavigatorObserver>? dipNavObservers; 14 final List<NavigatorObserver>? dipNavObservers;
14 final TransitionDelegate<dynamic>? transitionDelegate; 15 final TransitionDelegate<dynamic>? transitionDelegate;
@@ -19,15 +20,27 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -19,15 +20,27 @@ class GetDelegate extends RouterDelegate<GetPage>
19 GetDelegate( 20 GetDelegate(
20 {this.notFoundRoute, this.dipNavObservers, this.transitionDelegate}); 21 {this.notFoundRoute, this.dipNavObservers, this.transitionDelegate});
21 22
  23 + List<GetPage> getVisiblePages() {
  24 + return routes.where((r) {
  25 + final mware =
  26 + (r.middlewares ?? []).whereType<RouterOutletContainerMiddleWare>();
  27 + if (mware.length == 0) return true;
  28 + return r.name == mware.first.stayAt;
  29 + }).toList();
  30 + }
  31 +
22 /// Called by the [Router] at startup with the structure that the 32 /// Called by the [Router] at startup with the structure that the
23 /// [RouteInformationParser] obtained from parsing the initial route. 33 /// [RouteInformationParser] obtained from parsing the initial route.
24 @override 34 @override
25 Widget build(BuildContext context) { 35 Widget build(BuildContext context) {
  36 + final pages = getVisiblePages();
26 return Navigator( 37 return Navigator(
27 key: navigatorKey, 38 key: navigatorKey,
28 onPopPage: _onPopPage, 39 onPopPage: _onPopPage,
29 - pages: routes.toList(),  
30 - observers: [GetObserver()], 40 + pages: pages,
  41 + observers: [
  42 + GetObserver(),
  43 + ],
31 transitionDelegate: 44 transitionDelegate:
32 transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(), 45 transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
33 ); 46 );
@@ -73,13 +86,12 @@ class GetDelegate extends RouterDelegate<GetPage> @@ -73,13 +86,12 @@ class GetDelegate extends RouterDelegate<GetPage>
73 } 86 }
74 87
75 GetPage _notFound() { 88 GetPage _notFound() {
76 - return notFoundRoute ??  
77 - GetPage(  
78 - name: '/404',  
79 - page: () => Scaffold(  
80 - body: Text('not found'),  
81 - ),  
82 - ); 89 + return notFoundRoute ??= GetPage(
  90 + name: '/404',
  91 + page: () => Scaffold(
  92 + body: Text('not found'),
  93 + ),
  94 + );
83 } 95 }
84 96
85 Future<T?> pushRoute<T>( 97 Future<T?> pushRoute<T>(
1 -import 'dart:js';  
2 -  
3 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
  2 +import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
4 import '../../../get.dart'; 3 import '../../../get.dart';
5 4
6 class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object> 5 class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object>
@@ -23,13 +22,13 @@ class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object> @@ -23,13 +22,13 @@ class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object>
23 TDelegate? delegate, 22 TDelegate? delegate,
24 required List<T> Function(TDelegate routerDelegate) currentNavStack, 23 required List<T> Function(TDelegate routerDelegate) currentNavStack,
25 required List<T> Function(List<T> currentNavStack) pickPages, 24 required List<T> Function(List<T> currentNavStack) pickPages,
26 - required Widget Function(T? page) pageBuilder, 25 + required Widget Function(TDelegate, T? page) pageBuilder,
27 }) : this.builder( 26 }) : this.builder(
28 builder: (context, rDelegate, currentConfig) { 27 builder: (context, rDelegate, currentConfig) {
29 final currentStack = currentNavStack(rDelegate); 28 final currentStack = currentNavStack(rDelegate);
30 final picked = pickPages(currentStack); 29 final picked = pickPages(currentStack);
31 - if (picked.length == 0) return pageBuilder(null);  
32 - return pageBuilder(picked.last); 30 + if (picked.length == 0) return pageBuilder(rDelegate, null);
  31 + return pageBuilder(rDelegate, picked.last);
33 }, 32 },
34 delegate: delegate, 33 delegate: delegate,
35 ); 34 );
@@ -66,3 +65,53 @@ class _RouterOutletState<TDelegate extends RouterDelegate<T>, T extends Object> @@ -66,3 +65,53 @@ class _RouterOutletState<TDelegate extends RouterDelegate<T>, T extends Object>
66 return widget.builder(context, delegate, currentRoute); 65 return widget.builder(context, delegate, currentRoute);
67 } 66 }
68 } 67 }
  68 +
  69 +class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> {
  70 + GetRouterOutlet.builder({
  71 + required Widget Function(
  72 + BuildContext context,
  73 + GetDelegate delegate,
  74 + GetPage? currentRoute,
  75 + )
  76 + builder,
  77 + GetDelegate? routerDelegate,
  78 + }) : super.builder(
  79 + builder: builder,
  80 + delegate: routerDelegate,
  81 + );
  82 +
  83 + GetRouterOutlet({
  84 + Widget Function(GetDelegate delegate)? emptyStackPage,
  85 + required List<GetPage> Function(List<GetPage> currentNavStack) pickPages,
  86 + }) : super(
  87 + pageBuilder: (rDelegate, page) =>
  88 + (page?.page() ??
  89 + emptyStackPage?.call(rDelegate) ??
  90 + rDelegate.notFoundRoute?.page()) ??
  91 + SizedBox.shrink(),
  92 + currentNavStack: (routerDelegate) => routerDelegate.routes,
  93 + pickPages: pickPages,
  94 + delegate: Get.routerDelegate as GetDelegate,
  95 + );
  96 +}
  97 +
  98 +class RouterOutletContainerMiddleWare extends GetMiddleware {
  99 + final String stayAt;
  100 +
  101 + RouterOutletContainerMiddleWare(this.stayAt);
  102 + @override
  103 + RouteSettings? redirect(String? route) {
  104 + print('RouterOutletContainerMiddleWare: Redirect called ($route)');
  105 + return null;
  106 + }
  107 +}
  108 +
  109 +extension PagesListExt on List<GetPage> {
  110 + List<GetPage> pickAtRoute(String route) {
  111 + return skipWhile((value) => value.name != route).toList();
  112 + }
  113 +
  114 + List<GetPage> pickAfterRoute(String route) {
  115 + return skipWhile((value) => value.name != route).skip(1).toList();
  116 + }
  117 +}