Ahmed Fwela

WIP

successfully decoupled the logical routes from the visual routes
class DemoProduct {
final String name;
final String id;
DemoProduct({
required this.name,
required this.id,
});
}
... ...
import 'package:example_nav2/app/modules/home/controllers/home_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class DashboardView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'DashboardView is working',
style: TextStyle(fontSize: 20),
),
),
);
}
}
... ...
import 'package:example_nav2/app/modules/home/views/dashboard_view.dart';
import 'package:example_nav2/app/routes/app_pages.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
import 'package:get/get_navigation/src/nav2/router_outlet.dart';
import '../controllers/home_controller.dart';
class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return GetRouterOutlet.builder(
builder: (context, delegate, currentRoute) {
final title = currentRoute?.title;
final currentName = currentRoute?.name;
var currentIndex = 0;
if (currentName?.startsWith(Routes.PRODUCTS) == true) currentIndex = 2;
if (currentName?.startsWith(Routes.PROFILE) == true) currentIndex = 1;
return Scaffold(
appBar: AppBar(
title: Text('HomeView'),
appBar: title == null
? null
: AppBar(
title: Text(title),
centerTitle: true,
),
body: Center(
child: Text(
'HomeView is working',
style: TextStyle(fontSize: 20),
body: GetRouterOutlet(
emptyStackPage: (delegate) => DashboardView(),
pickPages: (currentNavStack) {
// will take any route after home
final res = currentNavStack.pickAfterRoute(Routes.HOME);
print('''RouterOutlet rebuild:
currentStack: $currentNavStack
pickedStack: $res''');
return res;
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (value) {
final getDelegate = Get.getDelegate();
if (getDelegate == null) return;
switch (value) {
case 0:
getDelegate.offUntil(Routes.HOME);
break;
case 1:
getDelegate.toNamed(Routes.PROFILE);
break;
case 2:
getDelegate.toNamed(Routes.PRODUCTS);
break;
default:
}
},
items: [
// Routes.Home + [Empty]
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
// Routes.Home + Routes.Profile
BottomNavigationBarItem(
icon: Icon(Icons.account_box_rounded),
label: 'Profile',
),
// Routes.Home + Routes.Products
BottomNavigationBarItem(
icon: Icon(Icons.account_box_rounded),
label: 'Products',
),
],
),
);
},
);
}
}
... ...
... ... @@ -5,8 +5,10 @@ import '../controllers/product_details_controller.dart';
class ProductDetailsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<ProductDetailsController>(
() => ProductDetailsController(),
Get.create<ProductDetailsController>(
() => ProductDetailsController(
Get.parameters['productId'] ?? '',
),
);
}
}
... ...
import 'package:get/get.dart';
class ProductDetailsController extends GetxController {}
class ProductDetailsController extends GetxController {
final String productId;
ProductDetailsController(this.productId);
}
... ...
... ... @@ -4,19 +4,21 @@ import 'package:get/get.dart';
import '../controllers/product_details_controller.dart';
class ProductDetailsView extends GetView<ProductDetailsController> {
class ProductDetailsView extends GetWidget<ProductDetailsController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProductDetailsView'),
centerTitle: true,
),
body: Center(
child: Text(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'ProductDetailsView is working',
style: TextStyle(fontSize: 20),
),
Text('ProductId: ${controller.productId}')
],
),
),
);
}
... ...
import 'package:example_nav2/app/models/demo_product.dart';
import 'package:get/get.dart';
class ProductsController extends GetxController {
//TODO: Implement ProductsController
final products = <DemoProduct>[].obs;
final count = 0.obs;
@override
void onInit() {
super.onInit();
void loadDemoProductsFromSomeWhere() {
products.add(
DemoProduct(
name: 'Product added on: ${DateTime.now().toString()}',
id: DateTime.now().millisecondsSinceEpoch.toString(),
),
);
}
@override
void onReady() {
super.onReady();
loadDemoProductsFromSomeWhere();
}
@override
void onClose() {}
void increment() => count.value++;
}
... ...
import 'package:example_nav2/app/routes/app_pages.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -8,14 +9,29 @@ class ProductsView extends GetView<ProductsController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProductsView'),
centerTitle: true,
floatingActionButton: FloatingActionButton.extended(
onPressed: controller.loadDemoProductsFromSomeWhere,
label: Text('Add'),
),
body: Obx(
() => RefreshIndicator(
onRefresh: () async {
controller.products.clear();
controller.loadDemoProductsFromSomeWhere();
},
child: ListView.builder(
itemCount: controller.products.length,
itemBuilder: (context, index) {
final item = controller.products[index];
return ListTile(
onTap: () {
Get.getDelegate()?.toNamed(Routes.PRODUCT_DETAILS(item.id));
},
title: Text(item.name),
subtitle: Text(item.id),
);
},
),
body: Center(
child: Text(
'ProductsView is working',
style: TextStyle(fontSize: 20),
),
),
);
... ...
... ... @@ -8,10 +8,6 @@ class ProfileView extends GetView<ProfileController> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ProfileView'),
centerTitle: true,
),
body: Center(
child: Text(
'ProfileView is working',
... ...
import 'package:get/get.dart';
import 'package:get/get_navigation/src/nav2/router_outlet.dart';
import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart';
import '../modules/product_details/bindings/product_details_binding.dart';
... ... @@ -22,24 +22,34 @@ class AppPages {
GetPage(
name: _Paths.HOME,
page: () => HomeView(),
binding: HomeBinding(),
//TODO: don't group bindings in one place, and instead make each page use its own binding
bindings: [
HomeBinding(),
//These must use [Get.lazyPut] or [Get.create] because their view is created long after they are declared
ProfileBinding(),
ProductsBinding(),
ProductDetailsBinding(),
],
title: null,
middlewares: [
RouterOutletContainerMiddleWare(_Paths.HOME),
],
children: [
GetPage(
name: _Paths.PROFILE,
page: () => ProfileView(),
binding: ProfileBinding(),
),
],
title: 'Profile',
),
GetPage(
name: _Paths.PRODUCTS,
page: () => ProductsView(),
binding: ProductsBinding(),
title: 'Products',
children: [
GetPage(
name: _Paths.PRODUCT_DETAILS,
page: () => ProductDetailsView(),
binding: ProductDetailsBinding(),
),
],
),
],
),
... ...
... ... @@ -5,12 +5,12 @@ abstract class Routes {
Routes._();
static const HOME = _Paths.HOME;
static const PROFILE = _Paths.PROFILE;
static const PROFILE = _Paths.HOME + _Paths.PROFILE;
static const SETTINGS = _Paths.SETTINGS;
static const PRODUCTS = _Paths.PRODUCTS;
static PRODUCT_DETAILS(String productId) => '${_Paths.PRODUCTS}/$productId';
static const PRODUCTS = _Paths.HOME + _Paths.PRODUCTS;
static String PRODUCT_DETAILS(String productId) => '$PRODUCTS/$productId';
}
abstract class _Paths {
... ...
import 'package:flutter/widgets.dart';
import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
import '../../get_core/src/get_interface.dart';
import '../../route_manager.dart';
import 'get_instance.dart';
extension Inst on GetInterface {
... ... @@ -127,4 +129,6 @@ extension Inst on GetInterface {
/// Casts the stored router delegate to a desired type
TDelegate? delegate<TDelegate extends RouterDelegate<TPage>, TPage>() =>
routerDelegate as TDelegate?;
GetDelegate? getDelegate() => delegate<GetDelegate, GetPage>();
}
... ...
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/nav2/router_outlet.dart';
import '../../../get.dart';
import '../../../get_state_manager/src/simple/list_notifier.dart';
... ... @@ -8,7 +9,7 @@ class GetDelegate extends RouterDelegate<GetPage>
with ListenableMixin, ListNotifierMixin {
final List<GetPage> routes = <GetPage>[];
final GetPage? notFoundRoute;
GetPage? notFoundRoute;
final List<NavigatorObserver>? dipNavObservers;
final TransitionDelegate<dynamic>? transitionDelegate;
... ... @@ -19,15 +20,27 @@ class GetDelegate extends RouterDelegate<GetPage>
GetDelegate(
{this.notFoundRoute, this.dipNavObservers, this.transitionDelegate});
List<GetPage> getVisiblePages() {
return routes.where((r) {
final mware =
(r.middlewares ?? []).whereType<RouterOutletContainerMiddleWare>();
if (mware.length == 0) return true;
return r.name == mware.first.stayAt;
}).toList();
}
/// Called by the [Router] at startup with the structure that the
/// [RouteInformationParser] obtained from parsing the initial route.
@override
Widget build(BuildContext context) {
final pages = getVisiblePages();
return Navigator(
key: navigatorKey,
onPopPage: _onPopPage,
pages: routes.toList(),
observers: [GetObserver()],
pages: pages,
observers: [
GetObserver(),
],
transitionDelegate:
transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
);
... ... @@ -73,8 +86,7 @@ class GetDelegate extends RouterDelegate<GetPage>
}
GetPage _notFound() {
return notFoundRoute ??
GetPage(
return notFoundRoute ??= GetPage(
name: '/404',
page: () => Scaffold(
body: Text('not found'),
... ...
import 'dart:js';
import 'package:flutter/material.dart';
import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
import '../../../get.dart';
class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object>
... ... @@ -23,13 +22,13 @@ class RouterOutlet<TDelegate extends RouterDelegate<T>, T extends Object>
TDelegate? delegate,
required List<T> Function(TDelegate routerDelegate) currentNavStack,
required List<T> Function(List<T> currentNavStack) pickPages,
required Widget Function(T? page) pageBuilder,
required Widget Function(TDelegate, T? page) pageBuilder,
}) : this.builder(
builder: (context, rDelegate, currentConfig) {
final currentStack = currentNavStack(rDelegate);
final picked = pickPages(currentStack);
if (picked.length == 0) return pageBuilder(null);
return pageBuilder(picked.last);
if (picked.length == 0) return pageBuilder(rDelegate, null);
return pageBuilder(rDelegate, picked.last);
},
delegate: delegate,
);
... ... @@ -66,3 +65,53 @@ class _RouterOutletState<TDelegate extends RouterDelegate<T>, T extends Object>
return widget.builder(context, delegate, currentRoute);
}
}
class GetRouterOutlet extends RouterOutlet<GetDelegate, GetPage> {
GetRouterOutlet.builder({
required Widget Function(
BuildContext context,
GetDelegate delegate,
GetPage? currentRoute,
)
builder,
GetDelegate? routerDelegate,
}) : super.builder(
builder: builder,
delegate: routerDelegate,
);
GetRouterOutlet({
Widget Function(GetDelegate delegate)? emptyStackPage,
required List<GetPage> Function(List<GetPage> currentNavStack) pickPages,
}) : super(
pageBuilder: (rDelegate, page) =>
(page?.page() ??
emptyStackPage?.call(rDelegate) ??
rDelegate.notFoundRoute?.page()) ??
SizedBox.shrink(),
currentNavStack: (routerDelegate) => routerDelegate.routes,
pickPages: pickPages,
delegate: Get.routerDelegate as GetDelegate,
);
}
class RouterOutletContainerMiddleWare extends GetMiddleware {
final String stayAt;
RouterOutletContainerMiddleWare(this.stayAt);
@override
RouteSettings? redirect(String? route) {
print('RouterOutletContainerMiddleWare: Redirect called ($route)');
return null;
}
}
extension PagesListExt on List<GetPage> {
List<GetPage> pickAtRoute(String route) {
return skipWhile((value) => value.name != route).toList();
}
List<GetPage> pickAfterRoute(String route) {
return skipWhile((value) => value.name != route).skip(1).toList();
}
}
... ...