From c8680ca47aacb48a1ad903c67fc11a66c61cc8b9 Mon Sep 17 00:00:00 2001 From: zmtzawqlp Date: Thu, 12 Sep 2024 22:53:10 +0800 Subject: [PATCH] Support route interceptor;Add RouteLifecycleState provides lifecycle management for routes; Add GlobalNavigator manages global navigation actions --- CHANGELOG.md | 6 + README.md | 862 +++++++++++++++++- lib/ff_annotation_route_library.dart | 7 + lib/src/global_navigator.dart | 28 + lib/src/interceptor/extension.dart | 549 +++++++++++ .../navigator_with_interceptor.dart | 496 ++++++++++ lib/src/interceptor/route_interceptor.dart | 95 ++ lib/src/page.dart | 1 - .../route_lifecycle/extended_route_aware.dart | 61 ++ .../extended_route_observer.dart | 121 +++ .../route_lifecycle_state.dart | 81 ++ pubspec.yaml | 7 +- 12 files changed, 2292 insertions(+), 22 deletions(-) create mode 100644 lib/src/global_navigator.dart create mode 100644 lib/src/interceptor/extension.dart create mode 100644 lib/src/interceptor/navigator_with_interceptor.dart create mode 100644 lib/src/interceptor/route_interceptor.dart create mode 100644 lib/src/route_lifecycle/extended_route_aware.dart create mode 100644 lib/src/route_lifecycle/extended_route_observer.dart create mode 100644 lib/src/route_lifecycle/route_lifecycle_state.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 5562846..c156fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 3.1.0 + +* Support route interceptor +* Add `RouteLifecycleState` provides lifecycle management for routes +* Add `GlobalNavigator` manages global navigation actions + ## 3.0.0 * Breaking change: use `FFRouteSettings.builder` instead of `FFRouteSettings.widget` diff --git a/README.md b/README.md index 684d84c..861c052 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,294 @@ # ff_annotation_route_library -The library for ff_annotation_route,support both null-safety and non-null-safety. +[![pub package](https://img.shields.io/pub/v/ff_annotation_route.svg)](https://pub.dartlang.org/packages/ff_annotation_route) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/ff_annotation_route)](https://github.com/fluttercandies/ff_annotation_route/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/ff_annotation_route)](https://github.com/fluttercandies/ff_annotation_route/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/ff_annotation_route)](https://github.com/fluttercandies/ff_annotation_route/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/ff_annotation_route)](https://github.com/fluttercandies/ff_annotation_route/issues) flutter-candies + + +## Description + +The library for ff_annotation_route + +- [ff\_annotation\_route\_library](#ff_annotation_route_library) + - [Description](#description) + - [Usage](#usage) + - [Add packages to dependencies](#add-packages-to-dependencies) + - [Add annotation](#add-annotation) + - [Empty Constructor](#empty-constructor) + - [Constructor with arguments](#constructor-with-arguments) + - [FFRoute](#ffroute) + - [Generate Route File](#generate-route-file) + - [Environment](#environment) + - [Activate the plugin](#activate-the-plugin) + - [Execute command](#execute-command) + - [Command Parameter](#command-parameter) + - [Navigator 1.0](#navigator-10) + - [Main.dart](#maindart) + - [Push](#push) + - [Push name](#push-name) + - [Push name with arguments](#push-name-with-arguments) + - [Navigator 2.0](#navigator-20) + - [Main.dart](#maindart-1) + - [FFRouteInformationParser](#ffrouteinformationparser) + - [FFRouterDelegate](#ffrouterdelegate) + - [Push](#push-1) + - [Push name](#push-name-1) + - [Push name with arguments](#push-name-with-arguments-1) + - [GetX](#getx) + - [How to use](#how-to-use) + - [How to set the parameter of GetPageRoute](#how-to-set-the-parameter-of-getpageroute) + - [Functional Widget](#functional-widget) + - [How to use with functional\_widget?](#how-to-use-with-functional_widget) + - [Code Hints](#code-hints) + - [I can do without it, but you must have it](#i-can-do-without-it-but-you-must-have-it) + - [Interceptor](#interceptor) + - [Route Interceptor](#route-interceptor) + - [Implement `RouteInterceptor`](#implement-routeinterceptor) + - [Add interceptors Annotation](#add-interceptors-annotation) + - [Generate Mapping](#generate-mapping) + - [Complete configuration](#complete-configuration) + - [Global Interceptor](#global-interceptor) + - [Implement RouteInterceptor](#implement-routeinterceptor-1) + - [Complete configuration](#complete-configuration-1) + - [push route](#push-route) + - [Lifecycle](#lifecycle) + - [RouteLifecycleState](#routelifecyclestate) + - [ExtendedRouteObserver](#extendedrouteobserver) + - [GlobalNavigator](#globalnavigator) + +## Usage + +### Add packages to dependencies + +Add the package to `dependencies` in your project/packages's `pubspec.yaml` * null-safety ``` yaml -environment: - sdk: '>=2.12.0 <3.0.0' dependencies: - ff_annotation_route: ^2.0.0 + # add for a package + ff_annotation_route_core: any + # add only for a project + ff_annotation_route_library: any ``` -* non-null-safety - -``` yaml -environment: - sdk: '<2.12.0' -dependencies: - ff_annotation_route: ^2.0.1-non-null-safety -``` +Download with `flutter packages get` + +### Add annotation + +#### Empty Constructor + +```dart +import 'package:ff_annotation_route/ff_annotation_route.dart'; + +@FFRoute( + name: "fluttercandies://mainpage", + routeName: "MainPage", +) +class MainPage extends StatelessWidget +{ + // ... +} + +``` + +#### Constructor with arguments + +The tool will handle it. What you should take care is that provide import url by setting `argumentImports` if it has +class/enum argument.you can use `@FFAutoImport()` instead now. + +or you can use `--no-fast-mode` for now, it will add parameters refer import automatically. + +```dart +@FFAutoImport('hide TestMode2') +import 'package:example1/src/model/test_model.dart'; +@FFAutoImport() +import 'package:example1/src/model/test_model1.dart' hide TestMode3; +import 'package:ff_annotation_route_library/ff_annotation_route_library.dart'; + +@FFRoute( + name: 'flutterCandies://testPageE', + routeName: 'testPageE', + description: 'Show how to push new page with arguments(class)', + // argumentImports are still work for some cases which you can't use @FFAutoImport() + // argumentImports: [ + // 'import \'package:example1/src/model/test_model.dart\';', + // 'import \'package:example1/src/model/test_model1.dart\';', + // ], + exts: { + 'group': 'Complex', + 'order': 1, + }, +) +class TestPageE extends StatelessWidget { + const TestPageE({ + this.testMode = const TestMode( + id: 2, + isTest: false, + ), + this.testMode1, + }); + factory TestPageE.deafult() => TestPageE( + testMode: TestMode.deafult(), + ); + + factory TestPageE.required({@required TestMode testMode}) => TestPageE( + testMode: testMode, + ); + + final TestMode testMode; + final TestMode1 testMode1; +} +``` + +#### FFRoute + +| Parameter | Description | Default | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| name | The name of the route (e.g., "/settings") | required | +| showStatusBar | Whether to show the status bar. | true | +| routeName | The route name to track page. | '' | +| pageRouteType | The type of page route.(material, cupertino, transparent) | - | +| description | The description of the route. | '' | +| exts | The extend arguments. | - | +| argumentImports | The imports of arguments. For example, class/enum argument should provide import url. you can use @FFAutoImport() instead now. | - | +| codes | to support something can't write in annotation, it will be hadnled as a code when generate route. [see](https://github.com/fluttercandies/ff_annotation_route/tree/master/example_getx) | - | + + +### Generate Route File + +#### Environment + +Add dart bin into to your `$PATH`. + +`cache\dart-sdk\bin` + +[`pub-global`](https://dart.dev/tools/pub/cmd/pub-global) + +#### Activate the plugin + +`dart pub global activate ff_annotation_route` + +#### Execute command + +Go to your project's root and execute command. + +`ff_route [arguments]` + +#### Command Parameter + +Available commands: + +```markdown +-h, --[no-]help Help usage +-p, --path Flutter project root path + (defaults to ".") +-n, --name Routes constant class name. + (defaults to "Routes") +-o, --output The path of main project route file and helper file.It is relative to the lib directory +-g, --git scan git lib(you should specify package names and split multiple by ,) + --exclude-packages Exclude given packages from scanning + --routes-file-output The path of routes file. It is relative to the lib directory + --const-ignore The regular to ignore some route consts + --[no-]package Is this a package + --[no-]super-arguments Whether generate page arguments helper class +-s, --[no-]save Whether save the arguments into the local + It will execute the local arguments if run "ff_route" without any arguments + --[no-]null-safety enable null-safety + (defaults to on) + --[no-]arguments-case-sensitive arguments is case sensitive or not + (defaults to on) + --[no-]fast-mode fast-mode: only analyze base on single dart file, it's fast. + no-fast mode: analyze base on whole packages and sdk, support super parameters and add parameters refer import automatically. + (defaults to on) +``` +### Navigator 1.0 + +you can see full demo in [example](https://github.com/fluttercandies/ff_annotation_route/tree/master/example) +#### Main.dart + +```dart +import 'package:ff_annotation_route_library/ff_annotation_route_library.dart'; +import 'package:flutter/material.dart'; +import 'example_route.dart'; +import 'example_routes.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'ff_annotation_route demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + primarySwatch: Colors.blue, + ), + initialRoute: Routes.fluttercandiesMainpage, + onGenerateRoute: (RouteSettings settings) { + return onGenerateRoute( + settings: settings, + getRouteSettings: getRouteSettings, + routeSettingsWrapper: (FFRouteSettings ffRouteSettings) { + if (ffRouteSettings.name == Routes.fluttercandiesMainpage || + ffRouteSettings.name == + Routes.fluttercandiesDemogrouppage.name) { + return ffRouteSettings; + } + return ffRouteSettings.copyWith( + widget: CommonWidget( + child: ffRouteSettings.widget, + title: ffRouteSettings.routeName, + )); + }, + ); + }, + ); + } +} +``` + +#### Push + +##### Push name + +``` dart + Navigator.pushNamed(context, Routes.fluttercandiesMainpage /* fluttercandies://mainpage */); +``` + +##### Push name with arguments + +* `arguments` **MUST** be a `Map` +```dart + Navigator.pushNamed( + context, + Routes.flutterCandiesTestPageE, + arguments: { + constructorName: 'required', + 'testMode': const TestMode( + id: 100, + isTest: true, + ), + }, + ); +``` +* enable --super-arguments + +``` dart + Navigator.pushNamed( + context, + Routes.flutterCandiesTestPageE.name, + arguments: Routes.flutterCandiesTestPageE.requiredC( + testMode: const TestMode( + id: 100, + isTest: true, + ), + ), + ); +``` ### Navigator 2.0 -you can see full demo in example1 +you can see full demo in [example1](https://github.com/fluttercandies/ff_annotation_route/tree/master/example1) #### Main.dart ``` dart @@ -92,7 +358,7 @@ class MyApp extends StatelessWidget { location: Routes.fluttercandiesMainpage, ), ), - routeInformationParser: kIsWeb ? _ffRouteInformationParser : null, + routeInformationParser: _ffRouteInformationParser, routerDelegate: _ffRouterDelegate, ); } @@ -102,7 +368,7 @@ class MyApp extends StatelessWidget { #### FFRouteInformationParser It's working on Web when you type in browser or report to browser. A delegate that is used by the [Router] widget to parse a route information -into a configuration of type [RouteSettings]. +into a configuration of type [RouteSettings]. for example: @@ -111,7 +377,7 @@ for example: #### FFRouterDelegate -A delegate that is used by the [Router] widget to build and configure anavigating widget. +A delegate that is used by the [Router] widget to build and configure anavigating widget. It provides push/pop methods like [Navigator]. @@ -126,7 +392,7 @@ It provides push/pop methods like [Navigator]. ); ``` -you can find more demo in `test_page_c.dart`. +you can find more demo in [test_page_c.dart](https://github.com/fluttercandies/ff_annotation_route/tree/master/example1/lib/src/pages/simple/test_page_c.dart). #### Push @@ -152,7 +418,7 @@ you can find more demo in `test_page_c.dart`. ), ); ``` -* enable --supper-arguments +* enable --super-arguments ``` dart FFRouterDelegate.of(context).pushNamed( @@ -162,5 +428,563 @@ you can find more demo in `test_page_c.dart`. 'map': {'ddd': 'dddd'}, 'testMode': const TestMode(id: 1, isTest: true), } - ) + ) +``` + +### GetX + +#### How to use +Getx is supported, you just need to convert `FFRouteSettings` to `GetPageRoute` + +``` dart +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + title: 'ff_annotation_route demo', + debugShowCheckedModeBanner: false, + theme: ThemeData( + primarySwatch: Colors.blue, + ), + initialRoute: Routes.fluttercandiesMainpage.name, + onGenerateRoute: (RouteSettings settings) { + FFRouteSettings ffRouteSettings = getRouteSettings( + name: settings.name!, + arguments: settings.arguments as Map?, + notFoundPageBuilder: () => Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('not find page'), + ), + ), + ); + Bindings? binding; + if (ffRouteSettings.codes != null) { + binding = ffRouteSettings.codes!['binding'] as Bindings?; + } + + Transition? transition; + bool opaque = true; + if (ffRouteSettings.pageRouteType != null) { + switch (ffRouteSettings.pageRouteType) { + case PageRouteType.cupertino: + transition = Transition.cupertino; + break; + case PageRouteType.material: + transition = Transition.downToUp; + break; + case PageRouteType.transparent: + opaque = false; + break; + default: + } + } + + return GetPageRoute( + binding: binding, + opaque: opaque, + settings: ffRouteSettings, + transition: transition, + page: () => ffRouteSettings.builder(), + ); + }, + ); + } +} ``` + +#### How to set the parameter of GetPageRoute + +for example: +'Bindings' is not const class, so it can't write in annotation, but you can set it as following codes: + +1. define it in `codes` +2. add import url in `argumentImports` +3. get it in `onGenerateRoute` + +``` dart +@FFRoute( + name: "/BindingsPage", + routeName: 'BindingsPage', + description: 'how to use Bindings with Annotation.', + codes: { + 'binding': 'Bindings1()', + }, + argumentImports: [ + 'import \'package:example_getx/src/bindings/bindings1.dart\';' + ], +) + +``` + +``` dart + onGenerateRoute: (RouteSettings settings) { + FFRouteSettings ffRouteSettings = getRouteSettings( + name: settings.name!, + arguments: settings.arguments as Map?, + notFoundPageBuilder: () => Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('not find page'), + ), + ), + ); + Bindings? binding; + if (ffRouteSettings.codes != null) { + binding = ffRouteSettings.codes!['binding'] as Bindings?; + } + + Transition? transition; + bool opaque = true; + if (ffRouteSettings.pageRouteType != null) { + switch (ffRouteSettings.pageRouteType) { + case PageRouteType.cupertino: + transition = Transition.cupertino; + break; + case PageRouteType.material: + transition = Transition.downToUp; + break; + case PageRouteType.transparent: + opaque = false; + break; + default: + } + } + + return GetPageRoute( + binding: binding, + opaque: opaque, + settings: ffRouteSettings, + transition: transition, + page: () => ffRouteSettings.builder(), + ); + }, + +``` + + + +### Functional Widget + +#### How to use with [functional_widget](https://github.com/rrousselGit/functional_widget)? + +```dart +@swidget +@FFRoute( + name: 'flutterCandies://func1', + routeName: 'test-func-1', +) +Widget func1( + int a, + String? b, { + bool? c, + required double d, +}) { + return Container(); +} +``` + +[Simple code](example/lib/src/pages/func/func.dart) is here. + + +### Code Hints + +you can use route as 'Routes.flutterCandiesTestPageE', and see Code Hints from ide. + +* default + +``` dart + /// 'This is test page E.' + /// + /// [name] : 'flutterCandies://testPageE' + /// + /// [routeName] : 'testPageE' + /// + /// [description] : 'This is test page E.' + /// + /// [constructors] : + /// + /// TestPageE : [TestMode testMode, TestMode1 testMode1] + /// + /// TestPageE.deafult : [] + /// + /// TestPageE.required : [TestMode(required) testMode] + /// + /// [exts] : {group: Complex, order: 1} + static const String flutterCandiesTestPageE = 'flutterCandies://testPageE'; +``` + +* enable --super-arguments + +``` dart + /// 'This is test page E.' + /// + /// [name] : 'flutterCandies://testPageE' + /// + /// [routeName] : 'testPageE' + /// + /// [description] : 'This is test page E.' + /// + /// [constructors] : + /// + /// TestPageE : [TestMode testMode, TestMode1 testMode1] + /// + /// TestPageE.test : [] + /// + /// TestPageE.requiredC : [TestMode(required) testMode] + /// + /// [exts] : {group: Complex, order: 1} + static const _FlutterCandiesTestPageE flutterCandiesTestPageE = + _FlutterCandiesTestPageE(); + + class _FlutterCandiesTestPageE { + const _FlutterCandiesTestPageE(); + + String get name => 'flutterCandies://testPageE'; + + Map d( + {TestMode testMode = const TestMode(id: 2, isTest: false), + TestMode1 testMode1}) => + { + 'testMode': testMode, + 'testMode1': testMode1, + }; + + Map test() => const { + 'constructorName': 'test', + }; + + Map requiredC({@required TestMode testMode}) => + { + 'testMode': testMode, + 'constructorName': 'requiredC', + }; + + @override + String toString() => name; + } + +``` + +## I can do without it, but you must have it + +### Interceptor + +#### Route Interceptor + +##### Implement `RouteInterceptor` + +Implement a `RouteInterceptor` to intercept route transitions for a specific page based on your scenario. + +``` dart +class LoginInterceptor extends RouteInterceptor { + const LoginInterceptor(); + + @override + Future intercept( + String routeName, { + Object? arguments, + }) async { + if (!User().hasLogin) { + return RouteInterceptResult.complete( + routeName: Routes.fluttercandiesLoginPage.name, + ); + } + + return RouteInterceptResult.next( + routeName: routeName, + arguments: arguments, + ); + } +} +``` + +Here are the possible scenarios corresponding to `RouteInterceptResult.complete`, `RouteInterceptResult.next`, and `RouteInterceptResult.abort`: + +``` dart +/// Represents the possible actions a route interceptor can take +/// after being invoked during the route interception process. +enum RouteInterceptAction { + /// Stops the interception chain and cancels any further actions. + /// This indicates that the current interceptor has determined + /// that no route should be pushed, and the navigation process should be aborted. + abort, + + /// Moves to the next interceptor in the chain. + /// This indicates that the current interceptor does not want to handle + /// the route and delegates the decision to subsequent interceptors. + next, + + /// Completes the interception process and allows the route to be pushed. + /// This indicates that the current interceptor has handled the route + /// and the navigation should proceed as expected. + complete, +} + +``` + + +##### Add interceptors Annotation + +Add an interceptors annotation to the page for route interception. + +``` dart +@FFRoute( + name: 'fluttercandies://PageA', + routeName: 'PageA', + description: 'PageA', + interceptors: [ + LoginInterceptor(), + ], +) +class PageA extends StatefulWidget { + const PageA({Key? key}) : super(key: key); + + @override + State createState() => _PageAState(); +} +``` + +##### Generate Mapping + +Execute `ff_route` to generate the interceptor mapping. + + +``` dart +/// The routeInterceptors auto generated by https://github.com/fluttercandies/ff_annotation_route +const Map> routeInterceptors = + >{ + 'fluttercandies://PageA': [LoginInterceptor()], + 'fluttercandies://PageB': [ + LoginInterceptor(), + PermissionInterceptor() + ], +}; +``` + +##### Complete configuration + +``` dart +void main() { + RouteInterceptorManager().addAllRouteInterceptors(routeInterceptors); + runApp(const MyApp()); +} +``` + +#### Global Interceptor + +If you don’t want to add interceptors in the annotation, you can choose to use global interceptors. + +##### Implement RouteInterceptor + +You can write your logic here based on your specific scenario. + +``` dart +class GlobalLoginInterceptor extends RouteInterceptor { + const GlobalLoginInterceptor(); + @override + Future intercept(String routeName, + {Object? arguments}) async { + if (routeName == Routes.fluttercandiesPageB.name || + routeName == Routes.fluttercandiesPageA.name) { + if (!User().hasLogin) { + return RouteInterceptResult.complete( + routeName: Routes.fluttercandiesLoginPage.name, + ); + } + } + + return RouteInterceptResult.next( + routeName: routeName, + arguments: arguments, + ); + } +} +``` + +##### Complete configuration + +``` dart +void main() { + RouteInterceptorManager().addGlobalInterceptors([ + const GlobalLoginInterceptor(), + const GlobalPermissionInterceptor(), + ]); + runApp(const MyApp()); +} +``` + +#### push route + +1. You can use the `NavigatorWithInterceptorExtension` extension to call methods with `WithInterceptor`. +``` dart + Navigator.of(context).pushNamedWithInterceptor( + Routes.fluttercandiesPageA.name, + ); +``` + +2. Call the static methods of `NavigatorWithInterceptor`. + +``` dart + NavigatorWithInterceptor.pushNamed( + context, + Routes.fluttercandiesPageB.name, + ); +``` + +### Lifecycle + +#### RouteLifecycleState + +By inheriting from `RouteLifecycleState`, you can easily detect various states of the page. + +The `onPageShow` and `onPageHide` callbacks are only triggered when the current component is hosted by a `PageRoute`. + +``` dart +class _PageBState extends RouteLifecycleState { + @override + void onForeground() { + print('PageB onForeground'); + } + + @override + void onBackground() { + print('PageB onBackground'); + } + + @override + void onPageShow() { + print('PageB onPageShow'); + } + + @override + void onPageHide() { + print('PageB onPageHide'); + } + + @override + void onRouteShow() { + print('onRouteShow'); + } + + @override + void onRouteHide() { + print('onRouteHide'); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Page B'), + ), + body: GestureDetector( + onTap: () {}, + child: const Center( + child: Text('This is Page B'), + ), + ), + ); + } +} +``` + +#### ExtendedRouteObserver + +[ExtendedRouteObserver] is a utility class that extends the functionality of +Flutter's built-in RouteObserver. It allows for more advanced route +management and tracking in the navigation stack. This class maintains +an internal list of active routes and provides several utility methods +for route inspection and manipulation. + +Key features of [ExtendedRouteObserver]: +- Tracks all active routes in the navigation stack. +- Provides access to the top-most route via the `topRoute` getter. +- Allows checking if a specific route exists in the stack with `containsRoute()`. +- Enables retrieval of a route by its name using `getRouteByName()`. +- Notifies subscribers when a route is added or removed via `onRouteAdded` and `onRouteRemoved`. +- Supports custom actions when a route is added or removed via `onRouteAdd()` and `onRouteRemove()`. + +This class is useful in cases where global route tracking or advanced +navigation behavior is needed, such as: +- Monitoring which routes are currently active. +- Handling custom navigation logic based on the current route stack. +- Implementing a navigation history feature or a breadcrumb-style navigator. + +By leveraging this class, developers can gain better insight into and +control over their app's navigation state. + +``` dart + Widget build(BuildContext context) { + return MaterialApp( + navigatorObservers: [ExtendedRouteObserver()], + ); + } +``` + +### GlobalNavigator + +GlobalNavigator class is a utility class for managing global navigation actions. +It provides easy access to the Navigator and BuildContext from anywhere in the app. + + +`context` is a crucial part of `Flutter`, involving many key functionalities such as themes, routing, and dependency injection. Flutter’s design philosophy is based on the propagation of context through the widget tree, allowing context to access relevant information and functionalities, which helps maintain good component separation and maintainability. + +While it is possible to directly access the `Navigator` or `context` via a global `navigatorKey` in certain situations, it is generally not recommended to use this approach regularly, especially when Flutter’s recommended patterns (such as accessing via `context`) work well. + +This approach can introduce some potential issues: + +1. Violates Flutter’s Design Philosophy: Flutter’s original design is based on localized navigation and state management through `BuildContext`. Bypassing `context` with a global approach may lead to state management confusion and make the code harder to maintain. + +2. Potential Performance Issues: Accessing `context` globally may bypass Flutter’s optimization mechanisms, as Flutter relies on the context tree’s structure for efficient `UI` updates. + +3. Poor Maintainability: Relying on global navigation can make the code more difficult to understand and maintain, especially as the app grows larger. It may become hard to track navigation flow and state. + +``` dart +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + navigatorKey: GlobalNavigator.navigatorKey, + home: HomeScreen(), + ); + } +} +``` +``` dart + GlobalNavigator.navigator?.push( + MaterialPageRoute(builder: (context) => SecondScreen()), + ); +``` + +``` dart + showDialog( + context: GlobalNavigator.context!, + builder: (b) { + return AlertDialog( + title: const Text('Permission Denied'), + content: + Text('You do not have permission to access this page.'), + actions: [ + TextButton( + onPressed: () { + GlobalNavigator.navigator?.pop(); + }, + child: const Text('OK'), + ), + ], + ); + }, + ); +``` \ No newline at end of file diff --git a/lib/ff_annotation_route_library.dart b/lib/ff_annotation_route_library.dart index 8aaaadc..388840a 100644 --- a/lib/ff_annotation_route_library.dart +++ b/lib/ff_annotation_route_library.dart @@ -2,8 +2,15 @@ library ff_annotation_route_library; export 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; +export 'src/global_navigator.dart'; export 'src/helper.dart'; +export 'src/interceptor/extension.dart'; +export 'src/interceptor/navigator_with_interceptor.dart'; +export 'src/interceptor/route_interceptor.dart'; export 'src/page.dart'; export 'src/route_helper.dart'; export 'src/route_information_parser.dart'; +export 'src/route_lifecycle/extended_route_aware.dart'; +export 'src/route_lifecycle/extended_route_observer.dart'; +export 'src/route_lifecycle/route_lifecycle_state.dart'; export 'src/router_delegate.dart'; diff --git a/lib/src/global_navigator.dart b/lib/src/global_navigator.dart new file mode 100644 index 0000000..1ac4b40 --- /dev/null +++ b/lib/src/global_navigator.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +// GlobalNavigator class is a utility class for managing global navigation actions. +// It provides easy access to the Navigator and BuildContext from anywhere in the app. +class GlobalNavigator { + // Private constructor to prevent instantiation, ensuring this class is used statically. + GlobalNavigator._(); + + // GlobalKey that will be used to access the NavigatorState globally. + // This allows us to perform navigation without directly depending on a specific context. + static GlobalKey navigatorKey = GlobalKey(); + + // Getter for accessing the Navigator. It retrieves the Navigator widget from the current state. + // An assert is added to ensure that the navigatorKey is initialized before accessing it. + static Navigator? get navigator { + assert(navigatorKey.currentState != null, 'Navigator is not initialized.'); + return navigatorKey.currentState!.widget; + } + + // Getter for accessing the current BuildContext of the Navigator. + // Useful when you need to pass context to some widgets or perform actions that require context. + // Asserts ensure that the context is initialized before trying to use it. + static BuildContext? get context { + assert( + navigatorKey.currentContext != null, 'Navigator is not initialized.'); + return navigatorKey.currentContext; + } +} diff --git a/lib/src/interceptor/extension.dart b/lib/src/interceptor/extension.dart new file mode 100644 index 0000000..34c1b06 --- /dev/null +++ b/lib/src/interceptor/extension.dart @@ -0,0 +1,549 @@ +import 'dart:async'; + +import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; +import 'package:flutter/widgets.dart'; + +import 'route_interceptor.dart'; + +extension NavigatorWithInterceptorExtension on NavigatorState { + Future _intercept( + String routeName, { + Object? arguments, + }) async { + final RouteInterceptResult result = + await RouteInterceptorManager().intercept( + routeName, + arguments: arguments, + ); + if (result.action == RouteInterceptAction.abort) { + return null; + } + + return result; + } + + /// Push a named route onto the navigator that most tightly encloses the given + /// context. + /// + /// {@template flutter.widgets.navigator.pushNamed} + /// The route name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route and the previous route (if any) are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPush]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the route. + /// + /// To use [pushNamed], an [Navigator.onGenerateRoute] callback must be + /// provided, + /// {@endtemplate} + /// + /// {@template flutter.widgets.Navigator.pushNamed} + /// The provided `arguments` are passed to the pushed route via + /// [RouteSettings.arguments]. Any object can be passed as `arguments` (e.g. a + /// [String], [int], or an instance of a custom `MyRouteArguments` class). + /// Often, a [Map] is used to pass key-value pairs. + /// + /// The `arguments` may be used in [Navigator.onGenerateRoute] or + /// [Navigator.onUnknownRoute] to construct the route. + /// {@endtemplate} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _didPushButton() { + /// Navigator.pushNamed(context, '/settings'); + /// } + /// ``` + /// {@end-tool} + /// + /// {@tool snippet} + /// + /// The following example shows how to pass additional `arguments` to the + /// route: + /// + /// ```dart + /// void _showBerlinWeather() { + /// Navigator.pushNamed( + /// context, + /// '/weather', + /// arguments: { + /// 'city': 'Berlin', + /// 'country': 'Germany', + /// }, + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// {@tool snippet} + /// + /// The following example shows how to pass a custom Object to the route: + /// + /// ```dart + /// class WeatherRouteArguments { + /// WeatherRouteArguments({ required this.city, required this.country }); + /// final String city; + /// final String country; + /// + /// bool get isGermanCapital { + /// return country == 'Germany' && city == 'Berlin'; + /// } + /// } + /// + /// void _showWeather() { + /// Navigator.pushNamed( + /// context, + /// '/weather', + /// arguments: WeatherRouteArguments(city: 'Berlin', country: 'Germany'), + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushNamed], which pushes a route that can be restored + /// during state restoration. + Future pushNamedWithInterceptor( + String routeName, { + Object? arguments, + }) async { + final RouteInterceptResult? result = + await _intercept(routeName, arguments: arguments); + if (result == null) { + return null; + } + + return pushNamed(result.routeName, arguments: result.arguments); + } + + /// Push a named route onto the navigator that most tightly encloses the given + /// context. + /// + /// {@template flutter.widgets.navigator.restorablePushNamed} + /// Unlike [Route]s pushed via [pushNamed], [Route]s pushed with this method + /// are restored during state restoration according to the rules outlined + /// in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushNamed} + /// + /// {@template flutter.widgets.Navigator.restorablePushNamed.arguments} + /// The provided `arguments` are passed to the pushed route via + /// [RouteSettings.arguments]. Any object that is serializable via the + /// [StandardMessageCodec] can be passed as `arguments`. Often, a Map is used + /// to pass key-value pairs. + /// + /// The arguments may be used in [Navigator.onGenerateRoute] or + /// [Navigator.onUnknownRoute] to construct the route. + /// {@endtemplate} + /// + /// {@template flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// The method returns an opaque ID for the pushed route that can be used by + /// the [RestorableRouteFuture] to gain access to the actual [Route] object + /// added to the navigator and its return value. You can ignore the return + /// value of this method, if you do not care about the route object or the + /// route's return value. + /// {@endtemplate} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _showParisWeather() { + /// Navigator.restorablePushNamed( + /// context, + /// '/weather', + /// arguments: { + /// 'city': 'Paris', + /// 'country': 'France', + /// }, + /// ); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + Future restorablePushNamedWithInterceptor( + String routeName, { + Object? arguments, + }) async { + final RouteInterceptResult? result = + await _intercept(routeName, arguments: arguments); + if (result == null) { + return ''; + } + + return restorablePushNamed(result.routeName, + arguments: result.arguments); + } + + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the route named [routeName] and then disposing + /// the previous route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.pushReplacementNamed} + /// If non-null, `result` will be used as the result of the route that is + /// removed; the future that had been returned from pushing that old route + /// will complete with `result`. Routes such as dialogs or popup menus + /// typically use this mechanism to return the value selected by the user to + /// the widget that created their route. The type of `result`, if provided, + /// must match the type argument of the class of the old route (`TO`). + /// + /// The route name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route and the route below the removed route are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didReplace]). The removed route is notified once the + /// new route has finished animating (see [Route.didComplete]). The removed + /// route's exit animation is not run (see [popAndPushNamed] for a variant + /// that does animated the removed route). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the type of the return value of the old route. + /// + /// To use [pushReplacementNamed], a [Navigator.onGenerateRoute] callback must + /// be provided. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _switchToBrightness() { + /// Navigator.pushReplacementNamed(context, '/settings/brightness'); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushReplacementNamed], which pushes a replacement route that + /// can be restored during state restoration. + @optionalTypeArgs + Future pushReplacementNamedWithInterceptor( + String routeName, { + TO? result, + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = await _intercept( + routeName, + arguments: arguments, + ); + if (routeInterceptResult == null) { + return null; + } + return pushReplacementNamed( + routeInterceptResult.routeName, + arguments: routeInterceptResult.arguments, + result: result, + ); + } + + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the route named [routeName] and then disposing + /// the previous route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.restorablePushReplacementNamed} + /// Unlike [Route]s pushed via [pushReplacementNamed], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushReplacementNamed} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _switchToAudioVolume() { + /// Navigator.restorablePushReplacementNamed(context, '/settings/volume'); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + Future restorablePushReplacementNamedWithInterceptor< + T extends Object?, TO extends Object?>( + String routeName, { + TO? result, + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = + await _intercept(routeName, arguments: arguments); + if (routeInterceptResult == null) { + return ''; + } + + return restorablePushReplacementNamed( + routeInterceptResult.routeName, + arguments: routeInterceptResult.arguments, + result: result, + ); + } + + /// Pop the current route off the navigator that most tightly encloses the + /// given context and push a named route in its place. + /// + /// {@template flutter.widgets.navigator.popAndPushNamed} + /// The popping of the previous route is handled as per [pop]. + /// + /// The new route's name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route, the old route, and the route below the old route (if any) + /// are all notified (see [Route.didPop], [Route.didComplete], + /// [Route.didPopNext], [Route.didPush], and [Route.didChangeNext]). If the + /// [Navigator] has any [Navigator.observers], they will be notified as well + /// (see [NavigatorObserver.didPop] and [NavigatorObserver.didPush]). The + /// animations for the pop and the push are performed simultaneously, so the + /// route below may be briefly visible even if both the old route and the new + /// route are opaque (see [TransitionRoute.opaque]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the return value type of the old route. + /// + /// To use [popAndPushNamed], a [Navigator.onGenerateRoute] callback must be provided. + /// + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _selectAccessibility() { + /// Navigator.popAndPushNamed(context, '/settings/accessibility'); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePopAndPushNamed], which pushes a new route that can be + /// restored during state restoration. + @optionalTypeArgs + Future + popAndPushNamedWithInterceptor( + String routeName, { + TO? result, + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = await _intercept( + routeName, + arguments: arguments, + ); + if (routeInterceptResult == null) { + return null; + } + return popAndPushNamed( + routeInterceptResult.routeName, + arguments: routeInterceptResult.arguments, + result: result, + ); + } + + /// Pop the current route off the navigator that most tightly encloses the + /// given context and push a named route in its place. + /// + /// {@template flutter.widgets.navigator.restorablePopAndPushNamed} + /// Unlike [Route]s pushed via [popAndPushNamed], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.popAndPushNamed} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _selectNetwork() { + /// Navigator.restorablePopAndPushNamed(context, '/settings/network'); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + Future restorablePopAndPushNamedWithInterceptor( + String routeName, { + TO? result, + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = + await _intercept(routeName, arguments: arguments); + if (routeInterceptResult == null) { + return ''; + } + + return restorablePopAndPushNamed( + routeInterceptResult.routeName, + arguments: routeInterceptResult.arguments, + result: result, + ); + } + + /// Push the route with the given name onto the navigator that most tightly + /// encloses the given context, and then remove all the previous routes until + /// the `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// The predicate may be applied to the same route more than once if + /// [Route.willHandlePopInternally] is true. + /// + /// To remove routes until a route with a certain name, use the + /// [RoutePredicate] returned from [ModalRoute.withName]. + /// + /// To remove all the routes below the pushed route, use a [RoutePredicate] + /// that always returns false (e.g. `(Route route) => false`). + /// + /// The removed routes are removed without being completed, so this method + /// does not take a return value argument. + /// + /// The new route's name (`routeName`) will be passed to the + /// [Navigator.onGenerateRoute] callback. The returned route will be pushed + /// into the navigator. + /// + /// The new route and the route below the bottommost removed route (which + /// becomes the route below the new route) are notified (see [Route.didPush] + /// and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPush] and [NavigatorObserver.didRemove]). The + /// removed routes are disposed, without being notified, once the new route + /// has finished animating. The futures that had been returned from pushing + /// those routes will not complete. + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route. + /// + /// To use [pushNamedAndRemoveUntil], an [Navigator.onGenerateRoute] callback + /// must be provided. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _resetToCalendar() { + /// Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/')); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can + /// be restored during state restoration. + @optionalTypeArgs + Future pushNamedAndRemoveUntilWithInterceptor( + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = await _intercept( + newRouteName, + arguments: arguments, + ); + if (routeInterceptResult == null) { + return null; + } + return pushNamedAndRemoveUntil( + routeInterceptResult.routeName, + predicate, + arguments: routeInterceptResult.arguments, + ); + } + + /// Push the route with the given name onto the navigator that most tightly + /// encloses the given context, and then remove all the previous routes until + /// the `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.restorablePushNamedAndRemoveUntil} + /// Unlike [Route]s pushed via [pushNamedAndRemoveUntil], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _resetToOverview() { + /// Navigator.restorablePushNamedAndRemoveUntil(context, '/overview', ModalRoute.withName('/')); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + Future + restorablePushNamedAndRemoveUntilWithInterceptor( + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) async { + final RouteInterceptResult? routeInterceptResult = + await _intercept(newRouteName, arguments: arguments); + if (routeInterceptResult == null) { + return ''; + } + return restorablePushNamedAndRemoveUntil( + routeInterceptResult.routeName, + predicate, + arguments: routeInterceptResult.arguments, + ); + } +} diff --git a/lib/src/interceptor/navigator_with_interceptor.dart b/lib/src/interceptor/navigator_with_interceptor.dart new file mode 100644 index 0000000..95bd097 --- /dev/null +++ b/lib/src/interceptor/navigator_with_interceptor.dart @@ -0,0 +1,496 @@ +import 'package:flutter/material.dart'; +import 'extension.dart'; + +/// The Navigator static method with interceptor. +class NavigatorWithInterceptor { + NavigatorWithInterceptor._(); + + /// Push a named route onto the navigator that most tightly encloses the given + /// context. + /// + /// {@template flutter.widgets.navigator.pushNamed} + /// The route name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route and the previous route (if any) are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPush]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the route. + /// + /// To use [pushNamed], an [Navigator.onGenerateRoute] callback must be + /// provided, + /// {@endtemplate} + /// + /// {@template flutter.widgets.Navigator.pushNamed} + /// The provided `arguments` are passed to the pushed route via + /// [RouteSettings.arguments]. Any object can be passed as `arguments` (e.g. a + /// [String], [int], or an instance of a custom `MyRouteArguments` class). + /// Often, a [Map] is used to pass key-value pairs. + /// + /// The `arguments` may be used in [Navigator.onGenerateRoute] or + /// [Navigator.onUnknownRoute] to construct the route. + /// {@endtemplate} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _didPushButton() { + /// Navigator.pushNamed(context, '/settings'); + /// } + /// ``` + /// {@end-tool} + /// + /// {@tool snippet} + /// + /// The following example shows how to pass additional `arguments` to the + /// route: + /// + /// ```dart + /// void _showBerlinWeather() { + /// Navigator.pushNamed( + /// context, + /// '/weather', + /// arguments: { + /// 'city': 'Berlin', + /// 'country': 'Germany', + /// }, + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// {@tool snippet} + /// + /// The following example shows how to pass a custom Object to the route: + /// + /// ```dart + /// class WeatherRouteArguments { + /// WeatherRouteArguments({ required this.city, required this.country }); + /// final String city; + /// final String country; + /// + /// bool get isGermanCapital { + /// return country == 'Germany' && city == 'Berlin'; + /// } + /// } + /// + /// void _showWeather() { + /// Navigator.pushNamed( + /// context, + /// '/weather', + /// arguments: WeatherRouteArguments(city: 'Berlin', country: 'Germany'), + /// ); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushNamed], which pushes a route that can be restored + /// during state restoration. + @optionalTypeArgs + static Future pushNamed( + BuildContext context, + String routeName, { + Object? arguments, + }) async { + return Navigator.of(context).pushNamedWithInterceptor( + routeName, + arguments: arguments, + ); + } + + /// Push a named route onto the navigator that most tightly encloses the given + /// context. + /// + /// {@template flutter.widgets.navigator.restorablePushNamed} + /// Unlike [Route]s pushed via [pushNamed], [Route]s pushed with this method + /// are restored during state restoration according to the rules outlined + /// in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushNamed} + /// + /// {@template flutter.widgets.Navigator.restorablePushNamed.arguments} + /// The provided `arguments` are passed to the pushed route via + /// [RouteSettings.arguments]. Any object that is serializable via the + /// [StandardMessageCodec] can be passed as `arguments`. Often, a Map is used + /// to pass key-value pairs. + /// + /// The arguments may be used in [Navigator.onGenerateRoute] or + /// [Navigator.onUnknownRoute] to construct the route. + /// {@endtemplate} + /// + /// {@template flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// The method returns an opaque ID for the pushed route that can be used by + /// the [RestorableRouteFuture] to gain access to the actual [Route] object + /// added to the navigator and its return value. You can ignore the return + /// value of this method, if you do not care about the route object or the + /// route's return value. + /// {@endtemplate} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _showParisWeather() { + /// Navigator.restorablePushNamed( + /// context, + /// '/weather', + /// arguments: { + /// 'city': 'Paris', + /// 'country': 'France', + /// }, + /// ); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + static Future restorablePushNamed( + BuildContext context, + String routeName, { + Object? arguments, + }) async { + return Navigator.of(context).restorablePushNamedWithInterceptor( + routeName, + arguments: arguments, + ); + } + + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the route named [routeName] and then disposing + /// the previous route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.pushReplacementNamed} + /// If non-null, `result` will be used as the result of the route that is + /// removed; the future that had been returned from pushing that old route + /// will complete with `result`. Routes such as dialogs or popup menus + /// typically use this mechanism to return the value selected by the user to + /// the widget that created their route. The type of `result`, if provided, + /// must match the type argument of the class of the old route (`TO`). + /// + /// The route name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route and the route below the removed route are notified (see + /// [Route.didPush] and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didReplace]). The removed route is notified once the + /// new route has finished animating (see [Route.didComplete]). The removed + /// route's exit animation is not run (see [popAndPushNamed] for a variant + /// that does animated the removed route). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the type of the return value of the old route. + /// + /// To use [pushReplacementNamed], a [Navigator.onGenerateRoute] callback must + /// be provided. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _switchToBrightness() { + /// Navigator.pushReplacementNamed(context, '/settings/brightness'); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushReplacementNamed], which pushes a replacement route that + /// can be restored during state restoration. + @optionalTypeArgs + static Future pushReplacementNamed( + BuildContext context, + String routeName, { + TO? result, + Object? arguments, + }) async { + return Navigator.of(context).pushReplacementNamedWithInterceptor( + routeName, + arguments: arguments, + result: result, + ); + } + + /// Replace the current route of the navigator that most tightly encloses the + /// given context by pushing the route named [routeName] and then disposing + /// the previous route once the new route has finished animating in. + /// + /// {@template flutter.widgets.navigator.restorablePushReplacementNamed} + /// Unlike [Route]s pushed via [pushReplacementNamed], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushReplacementNamed} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _switchToAudioVolume() { + /// Navigator.restorablePushReplacementNamed(context, '/settings/volume'); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + static Future + restorablePushReplacementNamed( + BuildContext context, + String routeName, { + TO? result, + Object? arguments, + }) async { + return Navigator.of(context) + .restorablePushReplacementNamedWithInterceptor( + routeName, + arguments: arguments, + result: result, + ); + } + + /// Pop the current route off the navigator that most tightly encloses the + /// given context and push a named route in its place. + /// + /// {@template flutter.widgets.navigator.popAndPushNamed} + /// The popping of the previous route is handled as per [pop]. + /// + /// The new route's name will be passed to the [Navigator.onGenerateRoute] + /// callback. The returned route will be pushed into the navigator. + /// + /// The new route, the old route, and the route below the old route (if any) + /// are all notified (see [Route.didPop], [Route.didComplete], + /// [Route.didPopNext], [Route.didPush], and [Route.didChangeNext]). If the + /// [Navigator] has any [Navigator.observers], they will be notified as well + /// (see [NavigatorObserver.didPop] and [NavigatorObserver.didPush]). The + /// animations for the pop and the push are performed simultaneously, so the + /// route below may be briefly visible even if both the old route and the new + /// route are opaque (see [TransitionRoute.opaque]). + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route, + /// and `TO` is the return value type of the old route. + /// + /// To use [popAndPushNamed], a [Navigator.onGenerateRoute] callback must be provided. + /// + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _selectAccessibility() { + /// Navigator.popAndPushNamed(context, '/settings/accessibility'); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePopAndPushNamed], which pushes a new route that can be + /// restored during state restoration. + @optionalTypeArgs + static Future popAndPushNamed( + BuildContext context, + String routeName, { + TO? result, + Object? arguments, + }) async { + return Navigator.of(context).popAndPushNamedWithInterceptor( + routeName, + arguments: arguments, + result: result, + ); + } + + /// Pop the current route off the navigator that most tightly encloses the + /// given context and push a named route in its place. + /// + /// {@template flutter.widgets.navigator.restorablePopAndPushNamed} + /// Unlike [Route]s pushed via [popAndPushNamed], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.popAndPushNamed} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _selectNetwork() { + /// Navigator.restorablePopAndPushNamed(context, '/settings/network'); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + static Future + restorablePopAndPushNamed( + BuildContext context, + String routeName, { + TO? result, + Object? arguments, + }) async { + return Navigator.of(context) + .restorablePopAndPushNamedWithInterceptor( + routeName, + arguments: arguments, + result: result, + ); + } + + /// Push the route with the given name onto the navigator that most tightly + /// encloses the given context, and then remove all the previous routes until + /// the `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// The predicate may be applied to the same route more than once if + /// [Route.willHandlePopInternally] is true. + /// + /// To remove routes until a route with a certain name, use the + /// [RoutePredicate] returned from [ModalRoute.withName]. + /// + /// To remove all the routes below the pushed route, use a [RoutePredicate] + /// that always returns false (e.g. `(Route route) => false`). + /// + /// The removed routes are removed without being completed, so this method + /// does not take a return value argument. + /// + /// The new route's name (`routeName`) will be passed to the + /// [Navigator.onGenerateRoute] callback. The returned route will be pushed + /// into the navigator. + /// + /// The new route and the route below the bottommost removed route (which + /// becomes the route below the new route) are notified (see [Route.didPush] + /// and [Route.didChangeNext]). If the [Navigator] has any + /// [Navigator.observers], they will be notified as well (see + /// [NavigatorObserver.didPush] and [NavigatorObserver.didRemove]). The + /// removed routes are disposed, without being notified, once the new route + /// has finished animating. The futures that had been returned from pushing + /// those routes will not complete. + /// + /// Ongoing gestures within the current route are canceled when a new route is + /// pushed. + /// + /// Returns a [Future] that completes to the `result` value passed to [pop] + /// when the pushed route is popped off the navigator. + /// + /// The `T` type argument is the type of the return value of the new route. + /// + /// To use [pushNamedAndRemoveUntil], an [Navigator.onGenerateRoute] callback + /// must be provided. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.Navigator.pushNamed} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _resetToCalendar() { + /// Navigator.pushNamedAndRemoveUntil(context, '/calendar', ModalRoute.withName('/')); + /// } + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can + /// be restored during state restoration. + @optionalTypeArgs + static Future pushNamedAndRemoveUntil( + BuildContext context, + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) async { + return Navigator.of(context).pushNamedAndRemoveUntilWithInterceptor( + newRouteName, + predicate, + arguments: arguments, + ); + } + + /// Push the route with the given name onto the navigator that most tightly + /// encloses the given context, and then remove all the previous routes until + /// the `predicate` returns true. + /// + /// {@template flutter.widgets.navigator.restorablePushNamedAndRemoveUntil} + /// Unlike [Route]s pushed via [pushNamedAndRemoveUntil], [Route]s pushed with + /// this method are restored during state restoration according to the rules + /// outlined in the "State Restoration" section of [Navigator]. + /// {@endtemplate} + /// + /// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.arguments} + /// + /// {@macro flutter.widgets.Navigator.restorablePushNamed.returnValue} + /// + /// {@tool snippet} + /// + /// Typical usage is as follows: + /// + /// ```dart + /// void _resetToOverview() { + /// Navigator.restorablePushNamedAndRemoveUntil(context, '/overview', ModalRoute.withName('/')); + /// } + /// ``` + /// {@end-tool} + @optionalTypeArgs + static Future restorablePushNamedAndRemoveUntil( + BuildContext context, + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) async { + return Navigator.of(context) + .restorablePushNamedAndRemoveUntilWithInterceptor( + newRouteName, + predicate, + arguments: arguments, + ); + } +} diff --git a/lib/src/interceptor/route_interceptor.dart b/lib/src/interceptor/route_interceptor.dart new file mode 100644 index 0000000..3a148bd --- /dev/null +++ b/lib/src/interceptor/route_interceptor.dart @@ -0,0 +1,95 @@ +import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; + +/// Manages route interceptors globally and per route basis. +class RouteInterceptorManager { + factory RouteInterceptorManager() => _routeInterceptors; + RouteInterceptorManager._(); + static final RouteInterceptorManager _routeInterceptors = + RouteInterceptorManager._(); + + // Global interceptors, applied to all routes. + final List _interceptors = []; + + // Route-specific interceptors mapped by route name. + final Map> _interceptorMap = + >{}; + + /// Adds a global interceptor to be applied to all routes. + void addGlobalInterceptor(RouteInterceptor interceptor) { + _interceptors.add(interceptor); + } + + /// Adds a list of global interceptors to be applied to all routes. + void addGlobalInterceptors(List interceptors) { + _interceptors.addAll(interceptors); + } + + /// Adds interceptors for a specific route by its name. + void addRouteInterceptors( + String routeName, List interceptors) { + _interceptorMap[routeName] = interceptors; + } + + /// Merges another map of route-specific interceptors. + void addAllRouteInterceptors( + Map> interceptorsMap) { + _interceptorMap.addAll(interceptorsMap); + } + + /// Removes a global interceptor. + void removeGlobalInterceptor(RouteInterceptor interceptor) { + _interceptors.remove(interceptor); + } + + /// Removes all interceptors for a specific route by its name. + void removeRouteInterceptors(String routeName) { + _interceptorMap.remove(routeName); + } + + /// Clears all global interceptors. + void clearGlobalInterceptors() { + _interceptors.clear(); + } + + /// Clears all route-specific interceptors. + void clearRouteInterceptors() { + _interceptorMap.clear(); + } + + /// Runs the interceptors, starting with route-specific ones (if any) + /// and falling back to global interceptors. + /// Returns a [RouteInterceptResult] that includes the action taken by the interceptor. + Future intercept( + String routeName, { + Object? arguments, + }) async { + // If no route-specific interceptors are found, use global interceptors. + final List interceptors = + _interceptorMap[routeName] ??= _interceptors; + + RouteInterceptResult routeInterceptResult = RouteInterceptResult( + action: RouteInterceptAction.complete, + routeName: routeName, + arguments: arguments, + ); + + // Execute each interceptor in the chain. + for (final RouteInterceptor interceptor in interceptors) { + routeInterceptResult = await interceptor.intercept( + routeName, + arguments: arguments, + ); + + // If the interceptor returns anything other than 'next', break the chain. + if (routeInterceptResult.action != RouteInterceptAction.next) { + return routeInterceptResult; + } + + // Update the name and arguments for the next interceptor. + routeName = routeInterceptResult.routeName; + arguments = routeInterceptResult.arguments; + } + + return routeInterceptResult; + } +} diff --git a/lib/src/page.dart b/lib/src/page.dart index 2eda37e..2490d02 100644 --- a/lib/src/page.dart +++ b/lib/src/page.dart @@ -5,7 +5,6 @@ import 'package:ff_annotation_route_core/ff_annotation_route_core.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'route_helper.dart'; /// Navigator 1.0 diff --git a/lib/src/route_lifecycle/extended_route_aware.dart b/lib/src/route_lifecycle/extended_route_aware.dart new file mode 100644 index 0000000..3dc5b5a --- /dev/null +++ b/lib/src/route_lifecycle/extended_route_aware.dart @@ -0,0 +1,61 @@ +import 'package:flutter/widgets.dart'; + +abstract class ExtendedRouteAware implements RouteAware { + bool get isPage; + + /// Called when the app is going into foreground. + void onForeground() {} + + /// Called when the app is going into background. + void onBackground() {} + + /// Called when current page is shown. + void onPageShow() {} + + /// Called when current page is hide. + void onPageHide() {} + + /// Called when current route is shown. + void onRouteShow() {} + + /// Called when current route is hide. + void onRouteHide() {} + + /// Called when the top route has been popped off, and the current route + /// shows up. + @override + void didPopNext() { + if (isPage) { + onPageShow(); + } + onRouteShow(); + } + + /// Called when the current route has been pushed. + @override + void didPush() { + if (isPage) { + onPageShow(); + } + onRouteShow(); + } + + /// Called when the current route has been popped off. + @override + void didPop() { + if (isPage) { + onPageHide(); + } + onRouteHide(); + } + + /// Called when a new route has been pushed, and the current route is no + /// longer visible. + @override + void didPushNext() { + if (isPage) { + onPageHide(); + } + onRouteHide(); + } +} diff --git a/lib/src/route_lifecycle/extended_route_observer.dart b/lib/src/route_lifecycle/extended_route_observer.dart new file mode 100644 index 0000000..e99dcb9 --- /dev/null +++ b/lib/src/route_lifecycle/extended_route_observer.dart @@ -0,0 +1,121 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; + +/// [ExtendedRouteObserver] is a utility class that extends the functionality of +/// Flutter's built-in RouteObserver. It allows for more advanced route +/// management and tracking in the navigation stack. This class maintains +/// an internal list of active routes and provides several utility methods +/// for route inspection and manipulation. +/// +/// Key features of [ExtendedRouteObserver]: +/// - Tracks all active routes in the navigation stack. +/// - Provides access to the top-most route via the `topRoute` getter. +/// - Allows checking if a specific route exists in the stack with `containsRoute()`. +/// - Enables retrieval of a route by its name using `getRouteByName()`. +/// - Notifies subscribers when a route is added or removed via `onRouteAdded` and `onRouteRemoved`. +/// - Supports custom actions when a route is added or removed via `onRouteAdd()` and `onRouteRemove()`. +/// +/// This class is useful in cases where global route tracking or advanced +/// navigation behavior is needed, such as: +/// - Monitoring which routes are currently active. +/// - Handling custom navigation logic based on the current route stack. +/// - Implementing a navigation history feature or a breadcrumb-style navigator. +/// +/// By leveraging this class, developers can gain better insight into and +/// control over their app's navigation state. +class ExtendedRouteObserver extends RouteObserver> { + // Singleton factory constructor for the ExtendedRouteObserver + factory ExtendedRouteObserver() => _extendedRouteObserver; + + // Private named constructor + ExtendedRouteObserver._(); + + // Static instance for the singleton + static final ExtendedRouteObserver _extendedRouteObserver = + ExtendedRouteObserver._(); + + // A list to store the routes currently in the navigation stack + final List> _routes = >[]; + + // Public getter to access the list of routes + List> get routes => _routes; + + // Public getter to access the top-most route in the stack + Route? get topRoute => _routes.isNotEmpty ? _routes.last : null; + + // Notifier for route additions. External subscribers can listen for route additions. + final ValueNotifier?> onRouteAdded = + ValueNotifier?>(null); + + // Notifier for route removals. External subscribers can listen for route removals. + final ValueNotifier?> onRouteRemoved = + ValueNotifier?>(null); + + // Triggered when a new route is pushed onto the stack + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + _addRoute(route); // Add the new route to the internal list + } + + // Triggered when a route is popped off the stack + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + _removeRoute(route); // Remove the route from the internal list + } + + // Triggered when a route is removed (without being popped) + @override + void didRemove(Route route, Route? previousRoute) { + super.didRemove(route, previousRoute); + _removeRoute(route); // Remove the route from the internal list + } + + // Triggered when a route is replaced by another route + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + super.didReplace(newRoute: newRoute, oldRoute: oldRoute); + _removeRoute(oldRoute); // Remove the old route from the internal list + _addRoute(newRoute); // Add the new route to the internal list + } + + // Adds a route to the internal list if it's not null + void _addRoute(Route? route) { + if (route != null) { + _routes.add(route); + onRouteAdd(route); + onRouteAdded.value = route; + } + } + + // Removes a route from the internal list if it's not null + void _removeRoute(Route? route) { + if (route != null) { + _routes.remove(route); + onRouteRemove(route); + onRouteRemoved.value = route; + } + } + + // Hook for custom actions when a route is added to the stack. + // This can be overridden in subclasses to define specific behaviors. + void onRouteAdd(Route? route) {} + + // Hook for custom actions when a route is removed from the stack. + // This can be overridden in subclasses to define specific behaviors. + void onRouteRemove(Route? route) {} + + // Checks if a specific route exists in the current route stack + bool containsRoute(Route route) { + return _routes.contains(route); // Returns true if the route is in the stack + } + + // Retrieves a route by its name from the current route stack, returns null if not found + Route? getRouteByName(String routeName) { + // Searches for the first route with the matching name, or returns null if not found + return _routes.firstWhereOrNull( + (Route route) => route.settings.name == routeName, + ); + } +} diff --git a/lib/src/route_lifecycle/route_lifecycle_state.dart b/lib/src/route_lifecycle/route_lifecycle_state.dart new file mode 100644 index 0000000..0f01437 --- /dev/null +++ b/lib/src/route_lifecycle/route_lifecycle_state.dart @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; + +import 'extended_route_aware.dart'; +import 'extended_route_observer.dart'; + +@optionalTypeArgs +// RouteLifecycleState is an abstract class that provides lifecycle management for routes. +// It extends State and uses the ExtendedRouteAware mixin to monitor route changes. +// It also observes the app's lifecycle events through WidgetsBindingObserver. +abstract class RouteLifecycleState extends State + with ExtendedRouteAware, WidgetsBindingObserver { + // Stores the current modal route (could be any route type) + ModalRoute? _modalRoute; + + // Getter to access the current modal route + ModalRoute? get modalRoute => _modalRoute; + + // Stores the current page route (specific to PageRoute type) + PageRoute? _pageRoute; + + // Getter to access the current page route + PageRoute? get pageRoute => _pageRoute; + + // Checks if the current route is a page route + @override + bool get isPage => _pageRoute != null; + + // Called when the state is initialized + @override + void initState() { + super.initState(); + // Registers the WidgetsBindingObserver to listen for app lifecycle changes + WidgetsBinding.instance.addObserver(this); + } + + // Called when dependencies change, such as when the widget tree is rebuilt + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Get the current modal route from the context + _modalRoute = ModalRoute.of(context); + + // If the current route is a PageRoute, cast it to PageRoute + if (_modalRoute is PageRoute) { + _pageRoute = _modalRoute as PageRoute; + } + + // Subscribe this state to the route observer to track route changes + ExtendedRouteObserver().subscribe(this, _modalRoute!); + } + + // Called when the widget is disposed of + @override + void dispose() { + // Remove the WidgetsBindingObserver when the state is destroyed + WidgetsBinding.instance.removeObserver(this); + + // Unsubscribe from the route observer to stop tracking route changes + ExtendedRouteObserver().unsubscribe(this); + + super.dispose(); + } + + // Called when the app's lifecycle state changes (e.g., app is resumed or paused) + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + // Check if this route is the current one before handling lifecycle events + if (_modalRoute?.isCurrent == true) { + // If the app comes to the foreground, call onForeground() + if (state == AppLifecycleState.resumed) { + onForeground(); + } + // If the app goes to the background, call onBackground() + else if (state == AppLifecycleState.paused) { + onBackground(); + } + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 1367daa..962add8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ff_annotation_route_library description: The library for ff_annotation_route,support both null-safety and non-null-safety. -version: 3.0.0 +version: 3.1.0 homepage: https://github.com/fluttercandies/ff_annotation_route_library environment: @@ -8,6 +8,9 @@ environment: dependencies: collection: ^1.15.0 - ff_annotation_route_core: ^2.0.4 + ff_annotation_route_core: ^2.1.0 flutter: sdk: flutter +# dependency_overrides: +# ff_annotation_route_core: +# path: ../ff_annotation_route_core \ No newline at end of file