Ahmed Fwela

+Fixed middleware parsing

+Added redirectDelegate to GetMiddleware, which simplifies creating route guards
+Introduced `participatesInRootNavigator` which replaces the old `RouterOutletContainerMiddleWare`
+Introduced `PreventDuplicateHandlingMode.ReorderRoutes` and made it the default recommended option
+Added route guard example to `example_nav2 `
import 'package:get/get.dart';
import '../../services/auth_service.dart';
import '../routes/app_pages.dart';
class EnsureAuthMiddleware extends GetMiddleware {
@override
GetNavConfig? redirectDelegate(GetNavConfig route) {
if (!AuthService.to.isLoggedInValue) {
final newRoute = Routes.LOGIN_THEN(route.location!);
return GetNavConfig.fromRoute(newRoute);
}
return super.redirectDelegate(route);
}
}
class EnsureNotAuthedMiddleware extends GetMiddleware {
@override
GetNavConfig? redirectDelegate(GetNavConfig route) {
if (AuthService.to.isLoggedInValue) {
//NEVER navigate to auth screen, when user is already authed
return null;
//OR redirect user to another screen
//return GetNavConfig.fromRoute(Routes.PROFILE);
}
return super.redirectDelegate(route);
}
}
... ...
import 'package:get/get.dart';
import '../controllers/login_controller.dart';
class LoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<LoginController>(
() => LoginController(),
);
}
}
... ...
import 'package:get/get.dart';
class LoginController extends GetxController {
//TODO: Implement LoginController
final count = 0.obs;
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {}
void increment() => count.value++;
}
... ...
import 'package:example_nav2/app/routes/app_pages.dart';
import 'package:example_nav2/services/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/login_controller.dart';
class LoginView extends GetView<LoginController> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() {
final isLoggedIn = AuthService.to.isLoggedInValue;
return Text(
'You are currently:'
' ${isLoggedIn ? "Logged In" : "Not Logged In"}'
"\nIt's impossible to enter this "
"route when you are logged in!",
);
},
),
MaterialButton(
child: Text(
'Do LOGIN !!',
style: TextStyle(color: Colors.blue, fontSize: 20),
),
onPressed: () {
AuthService.to.login();
final thenTo = Get.getDelegate()!
.currentConfiguration!
.currentPage!
.parameter?['then'];
Get.getDelegate()!.toNamed(thenTo ?? Routes.HOME);
},
),
],
),
),
);
}
}
... ...
import 'package:example_nav2/models/demo_product.dart';
import 'package:get/get.dart';
import '../../../models/demo_product.dart';
class ProductsController extends GetxController {
final products = <DemoProduct>[].obs;
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
class ProfileView extends GetView<ProfileController> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text(
'ProfileView is working',
style: TextStyle(fontSize: 20),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/profile_controller.dart';
class ProfileView extends GetView<ProfileController> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.amber,
body: Center(
child: Text(
'ProfileView is working',
style: TextStyle(fontSize: 20),
),
),
);
}
}
... ...
import 'package:example_nav2/services/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -35,6 +36,37 @@ class DrawerWidget extends StatelessWidget {
Navigator.of(context).pop();
},
),
if (AuthService.to.isLoggedInValue)
ListTile(
title: Text(
'Logout',
style: TextStyle(
color: Colors.red,
),
),
onTap: () {
AuthService.to.logout();
Get.getDelegate()!.toNamed(Routes.LOGIN);
//to close the drawer
Navigator.of(context).pop();
},
),
if (!AuthService.to.isLoggedInValue)
ListTile(
title: Text(
'Login',
style: TextStyle(
color: Colors.blue,
),
),
onTap: () {
Get.getDelegate()!.toNamed(Routes.LOGIN);
//to close the drawer
Navigator.of(context).pop();
},
),
],
),
);
... ...
import 'package:example_nav2/app/middleware/auth_middleware.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/nav2/router_outlet.dart';
import 'package:example_nav2/app/modules/login/bindings/login_binding.dart';
import 'package:example_nav2/app/modules/login/views/login_view.dart';
import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart';
import '../modules/product_details/bindings/product_details_binding.dart';
... ... @@ -25,14 +29,21 @@ class AppPages {
GetPage(
name: '/',
page: () => RootView(),
middlewares: [
RouterOutletContainerMiddleWare('/'),
],
binding: RootBinding(),
participatesInRootNavigator: true,
children: [
GetPage(
middlewares: [
//only enter this route when not authed
EnsureNotAuthedMiddleware(),
],
name: _Paths.LOGIN,
page: () => LoginView(),
binding: LoginBinding(),
),
GetPage(
participatesInRootNavigator: true,
name: _Paths.HOME,
preventDuplicates: true,
page: () => HomeView(),
bindings: [
HomeBinding(),
... ... @@ -40,6 +51,10 @@ class AppPages {
title: null,
children: [
GetPage(
middlewares: [
//only enter this route when authed
EnsureAuthMiddleware(),
],
name: _Paths.PROFILE,
page: () => ProfileView(),
title: 'Profile',
... ... @@ -57,12 +72,17 @@ class AppPages {
name: _Paths.PRODUCT_DETAILS,
page: () => ProductDetailsView(),
binding: ProductDetailsBinding(),
middlewares: [
//only enter this route when authed
EnsureAuthMiddleware(),
],
),
],
),
],
),
GetPage(
participatesInRootNavigator: true,
name: _Paths.SETTINGS,
page: () => SettingsView(),
binding: SettingsBinding(),
... ...
... ... @@ -11,6 +11,9 @@ abstract class Routes {
static const PRODUCTS = _Paths.HOME + _Paths.PRODUCTS;
static String PRODUCT_DETAILS(String productId) => '$PRODUCTS/$productId';
static const LOGIN = _Paths.LOGIN;
static String LOGIN_THEN(String afterSuccessfulLogin) =>
'$LOGIN?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}';
}
abstract class _Paths {
... ... @@ -19,4 +22,5 @@ abstract class _Paths {
static const PROFILE = '/profile';
static const SETTINGS = '/settings';
static const PRODUCT_DETAILS = '/:productId';
static const LOGIN = '/login';
}
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
import 'app/routes/app_pages.dart';
void main() {
runApp(
GetMaterialApp.router(
title: "Application",
getPages: AppPages.routes,
routeInformationParser: GetInformationParser(
// initialRoute: Routes.HOME,
),
routerDelegate: GetDelegate(
backButtonPopMode: PopMode.History,
preventDuplicateHandlingMode:
PreventDuplicateHandlingMode.PopUntilOriginalRoute,
),
),
);
}
import 'package:example_nav2/services/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_navigation/src/nav2/get_router_delegate.dart';
import 'app/routes/app_pages.dart';
void main() {
runApp(
GetMaterialApp.router(
title: "Application",
initialBinding: BindingsBuilder(
() {
Get.put(AuthService());
},
),
getPages: AppPages.routes,
routeInformationParser: GetInformationParser(
// initialRoute: Routes.HOME,
),
routerDelegate: GetDelegate(
backButtonPopMode: PopMode.History,
preventDuplicateHandlingMode:
PreventDuplicateHandlingMode.ReorderRoutes,
),
),
);
}
... ...
class DemoProduct {
final String name;
final String id;
DemoProduct({
required this.name,
required this.id,
});
}
class DemoProduct {
final String name;
final String id;
DemoProduct({
required this.name,
required this.id,
});
}
... ...
import 'package:get/get.dart';
class AuthService extends GetxService {
static AuthService get to => Get.find();
/// Mocks a login process
final isLoggedIn = false.obs;
bool get isLoggedInValue => isLoggedIn.value;
void login() {
isLoggedIn.value = true;
}
void logout() {
isLoggedIn.value = false;
}
}
... ...
... ... @@ -29,6 +29,16 @@ class GetNavConfig extends RouteInformation {
);
}
static GetNavConfig? fromRoute(String route) {
final res = Get.routeTree.matchRoute(route);
if (res.treeBranch.isEmpty) return null;
return GetNavConfig(
currentTreeBranch: res.treeBranch,
location: route,
state: null,
);
}
@override
String toString() => '''
======GetNavConfig=====\ncurrentTreeBranch: $currentTreeBranch\ncurrentPage: $currentPage\n======GetNavConfig=====''';
... ...
... ... @@ -38,6 +38,12 @@ enum PreventDuplicateHandlingMode {
/// Simply don't push the new route
DoNothing,
/// Recommended - Moves the old route entry to the front
///
/// With this mode, you guarantee there will be only one
/// route entry for each location
ReorderRoutes
}
class GetDelegate extends RouterDelegate<GetNavConfig>
... ... @@ -60,9 +66,47 @@ class GetDelegate extends RouterDelegate<GetNavConfig>
this.navigatorObservers,
this.transitionDelegate,
this.backButtonPopMode = PopMode.History,
this.preventDuplicateHandlingMode = PreventDuplicateHandlingMode.DoNothing,
this.preventDuplicateHandlingMode =
PreventDuplicateHandlingMode.ReorderRoutes,
});
GetNavConfig? runMiddleware(GetNavConfig config) {
final middlewares = config.currentTreeBranch.last.middlewares ?? [];
var iterator = config;
for (var item in middlewares) {
var redirectRes = item.redirectDelegate(iterator);
if (redirectRes == null) return null;
iterator = redirectRes;
}
return iterator;
}
void _unsafeHistoryAdd(GetNavConfig config) {
final res = runMiddleware(config);
if (res == null) return;
history.add(res);
}
void _unsafeHistoryRemove(GetNavConfig config) {
var index = history.indexOf(config);
if (index >= 0) _unsafeHistoryRemoveAt(index);
}
GetNavConfig? _unsafeHistoryRemoveAt(int index) {
if (index == history.length - 1) {
//removing WILL update the current route
final toCheck = history[history.length - 2];
final resMiddleware = runMiddleware(toCheck);
if (resMiddleware == null) return null;
history[history.length - 2] = resMiddleware;
}
return history.removeAt(index);
}
void _unsafeHistoryClear() {
history.clear();
}
/// Adds a new history entry and waits for the result
Future<T?> pushHistory<T>(
GetNavConfig config, {
... ... @@ -79,25 +123,32 @@ class GetDelegate extends RouterDelegate<GetNavConfig>
}
void _removeHistoryEntry(GetNavConfig entry) {
history.remove(entry);
_unsafeHistoryRemove(entry);
final lastCompleter = _resultCompleter.remove(entry);
lastCompleter?.complete(entry);
}
void _pushHistory(GetNavConfig config) {
if (config.currentPage!.preventDuplicates) {
if (history.any((element) => element.location == config.location)) {
final originalEntryIndex =
history.indexWhere((element) => element.location == config.location);
if (originalEntryIndex >= 0) {
switch (preventDuplicateHandlingMode) {
case PreventDuplicateHandlingMode.PopUntilOriginalRoute:
until(config.location!, popMode: PopMode.Page);
return;
break;
case PreventDuplicateHandlingMode.ReorderRoutes:
_unsafeHistoryRemoveAt(originalEntryIndex);
_unsafeHistoryAdd(config);
break;
case PreventDuplicateHandlingMode.DoNothing:
default:
return;
break;
}
return;
}
}
history.add(config);
_unsafeHistoryAdd(config);
}
// GetPageRoute getPageRoute(RouteSettings? settings) {
... ... @@ -110,9 +161,8 @@ class GetDelegate extends RouterDelegate<GetNavConfig>
return _doPopHistory();
}
GetNavConfig _doPopHistory() {
final res = history.removeLast();
return res;
GetNavConfig? _doPopHistory() {
return _unsafeHistoryRemoveAt(history.length - 1);
}
GetNavConfig? _popPage() {
... ... @@ -206,12 +256,18 @@ class GetDelegate extends RouterDelegate<GetNavConfig>
List<GetPage> getVisualPages() {
final currentHistory = currentConfiguration;
if (currentHistory == null) return <GetPage>[];
return currentHistory.currentTreeBranch.where((r) {
final mware =
(r.middlewares ?? []).whereType<RouterOutletContainerMiddleWare>();
if (mware.length == 0) return true;
return r.name == mware.first.stayAt;
}).toList();
final res = currentHistory.currentTreeBranch
.where((r) => r.participatesInRootNavigator != null);
if (res.length == 0) {
//default behavoir, all routes participate in root navigator
return currentHistory.currentTreeBranch;
} else {
//user specified at least one participatesInRootNavigator
return res
.where((element) => element.participatesInRootNavigator == true)
.toList();
}
}
@override
... ... @@ -234,7 +290,7 @@ class GetDelegate extends RouterDelegate<GetNavConfig>
@override
Future<void> setInitialRoutePath(GetNavConfig configuration) async {
history.clear();
_unsafeHistoryClear();
_resultCompleter.clear();
await pushHistory(configuration);
}
... ...
... ... @@ -120,15 +120,6 @@ class GetRouterOutlet extends RouterOutlet<GetDelegate, GetNavConfig> {
);
}
/// A marker outlet to identify which pages are visual
/// (handled by the navigator) and which are logical
/// (handled by the delegate)
class RouterOutletContainerMiddleWare extends GetMiddleware {
final String stayAt;
RouterOutletContainerMiddleWare(this.stayAt);
}
extension PagesListExt on List<GetPage> {
List<GetPage> pickAtRoute(String route) {
return skipWhile((value) => value.name != route).toList();
... ...
... ... @@ -98,14 +98,28 @@ class ParseRouteTree {
final parentPath = route.name;
for (var page in route.children!) {
// Add Parent middlewares to children
final pageMiddlewares = page.middlewares ?? <GetMiddleware>[];
pageMiddlewares.addAll(route.middlewares ?? <GetMiddleware>[]);
result.add(_addChild(page, parentPath, pageMiddlewares));
final parentMiddlewares = [
if (page.middlewares != null) ...page.middlewares!,
if (route.middlewares != null) ...route.middlewares!
];
result.add(
_addChild(
page,
parentPath,
parentMiddlewares,
),
);
final children = _flattenPage(page);
for (var child in children) {
pageMiddlewares.addAll(child.middlewares ?? <GetMiddleware>[]);
result.add(_addChild(child, parentPath, pageMiddlewares));
result.add(_addChild(
child,
parentPath,
[
...parentMiddlewares,
if (child.middlewares != null) ...child.middlewares!,
],
));
}
}
return result;
... ...
... ... @@ -34,6 +34,7 @@ class GetPage<T> extends Page<T> {
final String? title;
final Transition? transition;
final Curve curve;
final bool? participatesInRootNavigator;
final Alignment? alignment;
final bool maintainState;
final bool opaque;
... ... @@ -65,6 +66,7 @@ class GetPage<T> extends Page<T> {
required this.name,
required this.page,
this.title,
this.participatesInRootNavigator,
this.gestureWidth = 20,
// RouteSettings settings,
this.maintainState = true,
... ... @@ -82,7 +84,7 @@ class GetPage<T> extends Page<T> {
this.children,
this.middlewares,
this.unknownRoute,
this.preventDuplicates = false,
this.preventDuplicates = true,
}) : path = _nameToRegex(name),
super(
key: ValueKey(name),
... ... @@ -134,8 +136,11 @@ class GetPage<T> extends Page<T> {
List<GetMiddleware>? middlewares,
bool? preventDuplicates,
double? gestureWidth,
bool? participatesInRootNavigator,
}) {
return GetPage(
participatesInRootNavigator:
participatesInRootNavigator ?? this.participatesInRootNavigator,
preventDuplicates: preventDuplicates ?? this.preventDuplicates,
name: name ?? this.name,
page: page ?? this.page,
... ...
... ... @@ -32,6 +32,25 @@ abstract class _RouteMiddleware {
/// {@end-tool}
RouteSettings? redirect(String route);
/// Similar to [redirect],
/// This function will be called when the router delegate changes the
/// current route.
///
/// The default implmentation is to navigate to
/// the input route, with no redirection.
///
/// if this returns null, the navigation is stopped,
/// and no new routes are pushed.
/// {@tool snippet}
/// ```dart
/// GetNavConfig? redirect(GetNavConfig route) {
/// final authService = Get.find<AuthService>();
/// return authService.authed.value ? null : RouteSettings(name: '/login');
/// }
/// ```
/// {@end-tool}
GetNavConfig? redirectDelegate(GetNavConfig route);
/// This function will be called when this Page is called
/// you can use it to change something about the page or give it new page
/// {@tool snippet}
... ... @@ -97,6 +116,9 @@ class GetMiddleware implements _RouteMiddleware {
@override
void onPageDispose() {}
@override
GetNavConfig? redirectDelegate(GetNavConfig route) => route;
}
class MiddlewareRunner {
... ... @@ -204,6 +226,10 @@ class PageRedirect {
return GetPageRoute<T>(
page: _r.page,
parameter: _r.parameter,
alignment: _r.alignment,
title: _r.title,
maintainState: _r.maintainState,
routeName: _r.name,
settings: _r,
curve: _r.curve,
gestureWidth: _r.gestureWidth,
... ...
export 'context_extensions.dart';
export 'double_extensions.dart';
export 'duration_extensions.dart';
export 'dynamic_extensions.dart';
export 'event_loop_extensions.dart';
export 'internacionalization.dart';
export 'num_extensions.dart';
export 'string_extensions.dart';
export 'widget_extensions.dart';
export 'context_extensions.dart';
export 'double_extensions.dart';
export 'duration_extensions.dart';
export 'dynamic_extensions.dart';
export 'event_loop_extensions.dart';
export 'internacionalization.dart';
export 'num_extensions.dart';
export 'string_extensions.dart';
export 'widget_extensions.dart';
export 'iterable_extensions.dart';
... ...
extension IterableExtensions<T> on Iterable<T> {
Iterable<TRes> mapMany<TRes>(
Iterable<TRes>? Function(T item) selector) sync* {
for (var item in this) {
final res = selector(item);
if (res != null) yield* res;
}
}
}
... ...