diff --git a/examples/interceptors/.gitignore b/examples/interceptors/.gitignore
new file mode 100644
index 0000000..0fa6b67
--- /dev/null
+++ b/examples/interceptors/.gitignore
@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/examples/interceptors/.metadata b/examples/interceptors/.metadata
new file mode 100644
index 0000000..9cef17b
--- /dev/null
+++ b/examples/interceptors/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "b0850beeb25f6d5b10426284f506557f66181b36"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ - platform: web
+ create_revision: b0850beeb25f6d5b10426284f506557f66181b36
+ base_revision: b0850beeb25f6d5b10426284f506557f66181b36
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/examples/interceptors/README.md b/examples/interceptors/README.md
new file mode 100644
index 0000000..9ffb2bf
--- /dev/null
+++ b/examples/interceptors/README.md
@@ -0,0 +1,8 @@
+# Interceptors Example
+
+This example shows what you can achieve with BeamInterceptors. Interceptors are similar to BeamGuards, but interceptors can be dynamically added and removed from a BeamerDelegate.
+
+
+
+
+Run `flutter create .` to generate all necessary files, if needed.
diff --git a/examples/interceptors/analysis_options.yaml b/examples/interceptors/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/examples/interceptors/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/examples/interceptors/example-interceptors.gif b/examples/interceptors/example-interceptors.gif
new file mode 100644
index 0000000..c3ec807
Binary files /dev/null and b/examples/interceptors/example-interceptors.gif differ
diff --git a/examples/interceptors/lib/app/beam_intercept_example_app.dart b/examples/interceptors/lib/app/beam_intercept_example_app.dart
new file mode 100644
index 0000000..a24c23d
--- /dev/null
+++ b/examples/interceptors/lib/app/beam_intercept_example_app.dart
@@ -0,0 +1,21 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+import 'package:interceptors/main.dart';
+
+class BeamInterceptExampleApp extends StatelessWidget {
+ const BeamInterceptExampleApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp.router(
+ routerDelegate: beamerDelegate,
+ routeInformationParser: BeamerParser(),
+ backButtonDispatcher: BeamerBackButtonDispatcher(delegate: beamerDelegate),
+ debugShowCheckedModeBanner: false,
+ builder: (context, child) => Scaffold(
+ appBar: AppBar(),
+ body: child,
+ ),
+ );
+ }
+}
diff --git a/examples/interceptors/lib/main.dart b/examples/interceptors/lib/main.dart
new file mode 100644
index 0000000..f968431
--- /dev/null
+++ b/examples/interceptors/lib/main.dart
@@ -0,0 +1,38 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+import 'package:interceptors/app/beam_intercept_example_app.dart';
+import 'package:interceptors/stack/interceptor_stack_to_block.dart';
+import 'package:interceptors/stack/test_interceptor_stack.dart';
+
+final beamerDelegate = BeamerDelegate(
+ transitionDelegate: const NoAnimationTransitionDelegate(),
+ beamBackTransitionDelegate: const NoAnimationTransitionDelegate(),
+ stackBuilder: BeamerStackBuilder(
+ beamStacks: [
+ TestInterceptorStack(),
+ IntercepterStackToBlock(),
+ ],
+ ),
+);
+
+final BeamInterceptor allowNavigatingInterceptor = BeamInterceptor(
+ intercept: (context, delegate, currentPages, origin, target, deepLink) => false,
+ enabled: true, // this can be false too
+ name: 'allow',
+);
+
+final BeamInterceptor blockNavigatingInterceptor = BeamInterceptor(
+ intercept: (context, delegate, currentPages, origin, target, deepLink) => target is IntercepterStackToBlock,
+ enabled: true,
+ name: 'block',
+);
+
+void main() {
+ Beamer.setPathUrlStrategy();
+ runApp(
+ BeamerProvider(
+ routerDelegate: beamerDelegate,
+ child: const BeamInterceptExampleApp(),
+ ),
+ );
+}
diff --git a/examples/interceptors/lib/screens/allow_screen.dart b/examples/interceptors/lib/screens/allow_screen.dart
new file mode 100644
index 0000000..70fa956
--- /dev/null
+++ b/examples/interceptors/lib/screens/allow_screen.dart
@@ -0,0 +1,40 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+import 'package:interceptors/main.dart';
+
+class AllowScreen extends StatelessWidget {
+ const AllowScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return BeamInterceptorPopScope(
+ interceptors: [allowNavigatingInterceptor],
+ beamerDelegate: beamerDelegate,
+ child: Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ context.beamToNamed('/block-this-route');
+
+ ScaffoldMessenger.of(beamerDelegate.navigator.context).removeCurrentSnackBar();
+ ScaffoldMessenger.of(beamerDelegate.navigator.context).showSnackBar(
+ const SnackBar(content: Text('This route is NOT intercepted and thus NOT blocked.')),
+ );
+ },
+ child: const Text('Go to /block-this-route (not blocked)'),
+ ),
+ const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Go to back'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/interceptors/lib/screens/block_screen.dart b/examples/interceptors/lib/screens/block_screen.dart
new file mode 100644
index 0000000..0f84951
--- /dev/null
+++ b/examples/interceptors/lib/screens/block_screen.dart
@@ -0,0 +1,40 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+import 'package:interceptors/main.dart';
+
+class BlockScreen extends StatelessWidget {
+ const BlockScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return BeamInterceptorPopScope(
+ interceptors: [
+ blockNavigatingInterceptor,
+ ],
+ beamerDelegate: beamerDelegate,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ context.beamToNamed('/block-this-route');
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ ScaffoldMessenger.of(beamerDelegate.navigator.context).removeCurrentSnackBar();
+ ScaffoldMessenger.of(beamerDelegate.navigator.context).showSnackBar(
+ const SnackBar(content: Text('This route is intercepted and thus blocked.')),
+ );
+ });
+ },
+ child: const Text('Go to blocked route'),
+ ),
+ const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Go to back'),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/examples/interceptors/lib/stack/interceptor_stack_to_block.dart b/examples/interceptors/lib/stack/interceptor_stack_to_block.dart
new file mode 100644
index 0000000..19fa80c
--- /dev/null
+++ b/examples/interceptors/lib/stack/interceptor_stack_to_block.dart
@@ -0,0 +1,50 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+
+class IntercepterStackToBlock extends BeamStack {
+ @override
+ List get pathPatterns => ['/block-this-route'];
+
+ @override
+ List buildPages(BuildContext context, BeamState state) {
+ final pages = [
+ BeamPage(
+ key: const ValueKey('block-this-route'),
+ title: 'block-this-route',
+ type: BeamPageType.noTransition,
+ child: UnconstrainedBox(
+ child: SizedBox(
+ width: 500,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text('Super secret page', style: Theme.of(context).textTheme.titleLarge),
+ Padding(
+ padding: const EdgeInsets.only(top: 16),
+ child: SizedBox(
+ height: 50,
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () {
+ if (context.canBeamBack) {
+ context.beamBack();
+ } else if (Navigator.canPop(context)) {
+ Navigator.pop(context);
+ } else {
+ context.beamToNamed('/');
+ }
+ },
+ child: const Text('Go back'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ];
+
+ return pages;
+ }
+}
diff --git a/examples/interceptors/lib/stack/test_interceptor_stack.dart b/examples/interceptors/lib/stack/test_interceptor_stack.dart
new file mode 100644
index 0000000..910dc6a
--- /dev/null
+++ b/examples/interceptors/lib/stack/test_interceptor_stack.dart
@@ -0,0 +1,68 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/material.dart';
+import 'package:interceptors/screens/allow_screen.dart';
+import 'package:interceptors/screens/block_screen.dart';
+
+class TestInterceptorStack extends BeamStack {
+ @override
+ List get pathPatterns => ['/', '/allow', '/block'];
+
+ @override
+ List buildPages(BuildContext context, BeamState state) {
+ final pages = [
+ BeamPage(
+ key: const ValueKey('intercepter'),
+ title: 'Intercepter',
+ type: BeamPageType.noTransition,
+ child: UnconstrainedBox(
+ child: SizedBox(
+ width: 250,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: SizedBox(
+ height: 50,
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () => context.beamToNamed('/allow'),
+ child: const Text('Allow navigating to test'),
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8.0),
+ child: SizedBox(
+ height: 50,
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: () => context.beamToNamed('/block'),
+ child: const Text('Block navigating to test'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ if (state.uri.toString().contains('allow'))
+ const BeamPage(
+ key: ValueKey('allow'),
+ title: 'Allow',
+ type: BeamPageType.noTransition,
+ child: AllowScreen(),
+ ),
+ if (state.uri.toString().contains('block'))
+ const BeamPage(
+ key: ValueKey('block'),
+ title: 'Block',
+ type: BeamPageType.noTransition,
+ child: BlockScreen(),
+ ),
+ ];
+
+ return pages;
+ }
+}
diff --git a/examples/interceptors/pubspec.yaml b/examples/interceptors/pubspec.yaml
new file mode 100644
index 0000000..8194024
--- /dev/null
+++ b/examples/interceptors/pubspec.yaml
@@ -0,0 +1,22 @@
+name: interceptors
+description: Interceptors example app.
+
+publish_to: 'none'
+
+version: 1.0.0
+
+environment:
+ sdk: '>=3.0.0 <4.0.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+ beamer:
+ path: ../../package
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
diff --git a/package/.vscode/settings.json b/package/.vscode/settings.json
new file mode 100644
index 0000000..d50cea8
--- /dev/null
+++ b/package/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "dart.lineLength": 80
+}
\ No newline at end of file
diff --git a/package/lib/beamer.dart b/package/lib/beamer.dart
index a3c3343..53ae2af 100644
--- a/package/lib/beamer.dart
+++ b/package/lib/beamer.dart
@@ -1,13 +1,15 @@
library beamer;
+export 'src/beam_guard.dart';
+export 'src/beam_interceptor.dart';
+export 'src/beam_interceptor_scope.dart';
export 'src/beam_page.dart';
-export 'src/beam_state.dart';
export 'src/beam_stack.dart';
-export 'src/beam_guard.dart';
-export 'src/beamer_parser.dart';
-export 'src/beamer_delegate.dart';
+export 'src/beam_state.dart';
export 'src/beamer.dart';
-export 'src/beamer_provider.dart';
export 'src/beamer_back_button_dispatcher.dart';
+export 'src/beamer_delegate.dart';
+export 'src/beamer_parser.dart';
+export 'src/beamer_provider.dart';
export 'src/stack_builders.dart';
export 'src/transition_delegates.dart';
diff --git a/package/lib/src/beam_guard.dart b/package/lib/src/beam_guard.dart
index e5798e3..8173921 100644
--- a/package/lib/src/beam_guard.dart
+++ b/package/lib/src/beam_guard.dart
@@ -1,9 +1,7 @@
-import 'package:beamer/src/utils.dart';
-import 'package:flutter/widgets.dart';
-
-import 'package:beamer/src/beamer_delegate.dart';
-import 'package:beamer/src/beam_stack.dart';
import 'package:beamer/src/beam_page.dart';
+import 'package:beamer/src/beam_stack.dart';
+import 'package:beamer/src/beamer_delegate.dart';
+import 'package:flutter/widgets.dart';
/// Checks whether current [BeamStack] is allowed to be beamed to
/// and provides steps to be executed following a failed check.
@@ -37,7 +35,7 @@ class BeamGuard {
/// but will not match '/books'. To match '/books' and everything after it,
/// use '/books*'.
///
- /// See [_hasMatch] for more details.
+ /// See [BeamStack.shouldCheckGuard] for more details.
///
/// For RegExp:
/// You can use RegExp instances and the delegate will check for a match using [RegExp.hasMatch]
@@ -102,7 +100,8 @@ class BeamGuard {
/// Whether or not the guard should [check] the [stack].
bool shouldGuard(BeamStack stack) {
- return guardNonMatching ? !_hasMatch(stack) : _hasMatch(stack);
+ final shouldCheckGuard = stack.shouldCheckGuard(this);
+ return guardNonMatching ? shouldCheckGuard == false : shouldCheckGuard;
}
/// Applies the guard.
@@ -186,34 +185,4 @@ class BeamGuard {
return false;
}
-
- /// Matches [stack]'s pathBlueprint to [pathPatterns].
- ///
- /// If asterisk is present, it is enough that the pre-asterisk substring is
- /// contained within stack's pathPatterns.
- /// Else, the path (i.e. the pre-query substring) of the stack's uri
- /// must be equal to the pathPattern.
- bool _hasMatch(BeamStack stack) {
- for (final pathPattern in pathPatterns) {
- final path = stack.state.routeInformation.uri.path;
- if (pathPattern is String) {
- final asteriskIndex = pathPattern.indexOf('*');
- if (asteriskIndex != -1) {
- if (stack.state.routeInformation.uri
- .toString()
- .contains(pathPattern.substring(0, asteriskIndex))) {
- return true;
- }
- } else {
- if (pathPattern == path) {
- return true;
- }
- }
- } else {
- final regexp = Utils.tryCastToRegExp(pathPattern);
- return regexp.hasMatch(path);
- }
- }
- return false;
- }
}
diff --git a/package/lib/src/beam_interceptor.dart b/package/lib/src/beam_interceptor.dart
new file mode 100644
index 0000000..fad57d7
--- /dev/null
+++ b/package/lib/src/beam_interceptor.dart
@@ -0,0 +1,48 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/widgets.dart';
+
+class BeamInterceptor {
+ /// Creates a [BeamInterceptor] with defined properties.
+ ///
+ /// [name] and [intercept] must not be null.
+ const BeamInterceptor({
+ this.enabled = true,
+ required this.name,
+ required this.intercept,
+ });
+
+ /// A name of the interceptor.
+ ///
+ /// It is used to compare interceptors.
+ final String name;
+
+ /// Whether the interceptor is enabled.
+ final bool enabled;
+
+ /// The interceptor function.
+ ///
+ /// Returns `true` if the interceptor should be applied and `false` otherwise.
+ ///
+ /// The interceptor can be disabled by setting [enabled] to `false`.
+ ///
+ /// The targetBeamStack is the [BeamStack] that is beeing pushed or popped to. (destination)
+ final bool Function(
+ BuildContext context,
+ BeamerDelegate delegate,
+ List currentPages,
+ BeamStack origin,
+ BeamStack target,
+ String? deepLink,
+ ) intercept;
+
+ @override
+ bool operator ==(other) {
+ if (other is! BeamInterceptor) {
+ return false;
+ }
+ return name == other.name;
+ }
+
+ @override
+ int get hashCode => name.hashCode;
+}
diff --git a/package/lib/src/beam_interceptor_scope.dart b/package/lib/src/beam_interceptor_scope.dart
new file mode 100644
index 0000000..d40f251
--- /dev/null
+++ b/package/lib/src/beam_interceptor_scope.dart
@@ -0,0 +1,70 @@
+import 'package:beamer/beamer.dart';
+import 'package:flutter/widgets.dart';
+
+/// This works like [PopScope], but with beam-interceptors.
+///
+/// See [BeamInterceptor] for more information.
+///
+/// If any of the interceptors return true, the pop will not be invoked.
+///
+/// This works on Navigator.pop as well as all the Beamer's beaming functions.
+///
+/// ```dart
+/// BeamInterceptorScope(
+/// interceptors: [BeamInterceptor(...), ...],
+/// child: Center(
+/// child: ElevatedButton(
+/// child: const Text('Go back'),
+/// onPressed: () => Beamer.of(context).beamToNamed('/'),
+/// ),
+/// ),
+/// );
+/// ```
+class BeamInterceptorScope extends StatefulWidget {
+ const BeamInterceptorScope({
+ required this.child,
+ required this.interceptors,
+ this.beamerDelegate,
+ super.key,
+ });
+
+ final Widget child;
+
+ /// The interceptors to check upon any beaming or popping.
+ final List interceptors;
+
+ /// The [BeamerDelegate] to apply the interceptors to.
+ final BeamerDelegate? beamerDelegate;
+
+ @override
+ State createState() => _BeamInterceptorScopeState();
+}
+
+class _BeamInterceptorScopeState extends State {
+ late BeamerDelegate beamerDelegate;
+
+ @override
+ void initState() {
+ super.initState();
+
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ beamerDelegate = widget.beamerDelegate ?? Beamer.of(context);
+
+ for (var interceptor in widget.interceptors) {
+ beamerDelegate.addInterceptor(interceptor);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ for (var interceptor in widget.interceptors) {
+ beamerDelegate.removeInterceptor(interceptor);
+ }
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => widget.child;
+}
diff --git a/package/lib/src/beam_page.dart b/package/lib/src/beam_page.dart
index 7199fb1..9e5581b 100644
--- a/package/lib/src/beam_page.dart
+++ b/package/lib/src/beam_page.dart
@@ -54,6 +54,8 @@ class BeamPage extends Page {
this.fullScreenDialog = false,
this.opaque = true,
this.keepQueryOnPop = false,
+ this.transitionDuration,
+ this.reverseTransitionDuration,
}) : super(key: key, name: name);
/// A [BeamPage] to be the default for [BeamerDelegate.notFoundPage].
@@ -206,6 +208,20 @@ class BeamPage extends Page {
/// See [BeamPageType] for available types.
final BeamPageType type;
+ /// The transition duration for this [BeamPage].
+ ///
+ /// Defaults to `Duration(milliseconds: 300)`. (use flutter's default)
+ ///
+ /// This is not used when [type] is [BeamPageType.cupertino] or [BeamPageType.material].
+ final Duration? transitionDuration;
+
+ /// The reverse transition duration for this [BeamPage].
+ ///
+ /// Defaults to `Duration(milliseconds: 300)`. (use flutter's default)
+ ///
+ /// This is not used when [type] is [BeamPageType.cupertino] or [BeamPageType.material].
+ final Duration? reverseTransitionDuration;
+
/// A builder for custom [Route] to use in [createRoute].
///
/// `context` is the build context.
@@ -249,6 +265,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
child: child,
@@ -260,6 +279,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: animation.drive(
Tween(begin: const Offset(0, 1), end: const Offset(0, 0))
@@ -273,6 +295,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: animation.drive(
Tween(begin: const Offset(1, 0), end: const Offset(0, 0))
@@ -286,6 +311,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: animation.drive(
Tween(begin: const Offset(-1, 0), end: const Offset(0, 0))
@@ -299,6 +327,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: animation.drive(
Tween(begin: const Offset(0, -1), end: const Offset(0, 0))
@@ -312,6 +343,9 @@ class BeamPage extends Page {
opaque: opaque,
settings: this,
pageBuilder: (_, __, ___) => child,
+ transitionDuration: transitionDuration ?? Duration(milliseconds: 300),
+ reverseTransitionDuration:
+ reverseTransitionDuration ?? Duration(milliseconds: 300),
transitionsBuilder: (_, animation, __, child) => ScaleTransition(
scale: animation,
child: child,
@@ -322,6 +356,8 @@ class BeamPage extends Page {
fullscreenDialog: fullScreenDialog,
opaque: opaque,
settings: this,
+ transitionDuration: transitionDuration ?? Duration.zero,
+ reverseTransitionDuration: reverseTransitionDuration ?? Duration.zero,
pageBuilder: (context, animation, secondaryAnimation) => child,
);
default:
diff --git a/package/lib/src/beam_stack.dart b/package/lib/src/beam_stack.dart
index aa322f3..acf60bb 100644
--- a/package/lib/src/beam_stack.dart
+++ b/package/lib/src/beam_stack.dart
@@ -318,7 +318,115 @@ abstract class BeamStack
/// Can this handle the [uri] based on its [pathPatterns].
///
/// Can be useful in a custom [BeamerDelegate.stackBuilder].
- bool canHandle(Uri uri) => Utils.canBeamStackHandleUri(this, uri);
+ bool canHandle(Uri uri) {
+ for (final pathPattern in pathPatterns) {
+ if (pathPattern is String) {
+ // If it is an exact match or asterisk pattern
+ if (pathPattern == uri.path ||
+ pathPattern == '/*' ||
+ pathPattern == '*') {
+ return true;
+ }
+
+ // Clean URI path segments
+ final uriPathSegments = uri.pathSegments.toList();
+ if (uriPathSegments.length > 1 && uriPathSegments.last == '') {
+ uriPathSegments.removeLast();
+ }
+
+ final pathPatternSegments = Uri.parse(pathPattern).pathSegments;
+
+ // If we're in strict mode and URI has fewer segments than pattern,
+ // we don't have a match so can continue.
+ if (strictPathPatterns &&
+ uriPathSegments.length < pathPatternSegments.length) {
+ continue;
+ }
+
+ // If URI has more segments and pattern doesn't end with asterisk,
+ // we don't have a match so can continue.
+ if (uriPathSegments.length > pathPatternSegments.length &&
+ (pathPatternSegments.isEmpty ||
+ !pathPatternSegments.last.endsWith('*'))) {
+ continue;
+ }
+
+ var checksPassed = true;
+ // Iterating through URI segments
+ for (var i = 0; i < uriPathSegments.length; i++) {
+ // If all checks have passed up to i,
+ // if pattern has no more segments to traverse and it ended with asterisk,
+ // it is a match and we can break,
+ if (pathPatternSegments.length < i + 1 &&
+ pathPatternSegments.last.endsWith('*')) {
+ checksPassed = true;
+ break;
+ }
+
+ // If pattern has asterisk at i-th position,
+ // anything matches and we can continue.
+ if (pathPatternSegments[i] == '*') {
+ continue;
+ }
+ // If they are not the same and pattern doesn't expects path parameter,
+ // there's no match and we can break.
+ if (uriPathSegments[i] != pathPatternSegments[i] &&
+ !pathPatternSegments[i].startsWith(':')) {
+ checksPassed = false;
+ break;
+ }
+ }
+ // If no check failed, beamStack can handle this URI.
+ if (checksPassed) {
+ return true;
+ }
+ } else {
+ final regex = pathPattern.toRegExp;
+ final hasMatch = regex.hasMatch(uri.toString());
+
+ if (hasMatch) {
+ return true;
+ } else {
+ continue;
+ }
+ }
+ }
+ return false;
+ }
+
+ /// Matches pathBlueprint to [pathPatterns].
+ ///
+ /// If asterisk is present, it is enough that the pre-asterisk substring is
+ /// contained within stack's pathPatterns.
+ /// Else, the path (i.e. the pre-query substring) of the stack's uri
+ /// must be equal to the pathPattern.
+ bool shouldCheckGuard(BeamGuard guard) {
+ for (final guardPathPattern in guard.pathPatterns) {
+ final uri = state.routeInformation.uri;
+ final path = uri.path;
+
+ if (guardPathPattern is! String) {
+ final regex = guardPathPattern.toRegExp;
+ final hasMatch = regex.hasMatch(path);
+
+ if (hasMatch) {
+ return true;
+ } else {
+ continue;
+ }
+ }
+
+ final asteriskIndex = guardPathPattern.indexOf('*');
+
+ if (asteriskIndex == -1) return guardPathPattern == path;
+
+ return uri
+ .toString()
+ .contains(guardPathPattern.substring(0, asteriskIndex));
+ }
+
+ return false;
+ }
/// Gives the ability to wrap the [navigator].
///
@@ -360,6 +468,8 @@ abstract class BeamStack
/// If this is false (default), then a path pattern '/some/path' will match
/// '/' and '/some' and '/some/path'.
/// If this is true, then it will match just '/some/path'.
+ ///
+ /// __This only applies if the pattern is of type STRING, not REGEXP__
bool get strictPathPatterns => false;
/// Creates and returns the list of pages to be built by the [Navigator]
@@ -577,10 +687,13 @@ class RoutesBeamStack extends BeamStack {
matched[route] = createMatch(path, uri.queryParameters);
}
} else {
- final regexp = Utils.tryCastToRegExp(route);
- if (regexp.hasMatch(uri.toString())) {
- final path = uri.toString();
- matched[regexp] = createMatch(path, uri.queryParameters);
+ final regex = route.toRegExp;
+ final path = uri.toString();
+
+ final hasMatch = regex.hasMatch(path);
+
+ if (hasMatch) {
+ matched[regex] = createMatch(path, uri.queryParameters);
}
}
}
diff --git a/package/lib/src/beamer.dart b/package/lib/src/beamer.dart
index f66d2d4..84d5ba6 100644
--- a/package/lib/src/beamer.dart
+++ b/package/lib/src/beamer.dart
@@ -1,8 +1,7 @@
import 'package:beamer/beamer.dart';
-import 'package:flutter/widgets.dart';
-
import 'package:beamer/src/path_url_strategy_nonweb.dart'
if (dart.library.html) 'path_url_strategy_web.dart' as url_strategy;
+import 'package:flutter/widgets.dart';
/// Represents a navigation area and is a wrapper for [Router].
///
@@ -48,6 +47,20 @@ class Beamer extends StatefulWidget {
}
}
+ /// Access Beamer's [routerDelegate].
+ ///
+ /// Giving `true` to [root] gets the root beamer if the closest beamer is
+ /// nested under another beamer.
+ ///
+ /// This is the same as `Beamer.of(context, root: true)`. But returns `null` if no Beamer is found in the context.
+ static BeamerDelegate? maybeOf(BuildContext context, {bool root = false}) {
+ try {
+ return of(context, root: root);
+ } catch (e) {
+ return null;
+ }
+ }
+
/// Change the strategy to use for handling browser URL to `PathUrlStrategy`.
///
/// `PathUrlStrategy` uses the browser URL's pathname to represent Beamer's route name.
diff --git a/package/lib/src/beamer_back_button_dispatcher.dart b/package/lib/src/beamer_back_button_dispatcher.dart
index 88fc547..328cdcf 100644
--- a/package/lib/src/beamer_back_button_dispatcher.dart
+++ b/package/lib/src/beamer_back_button_dispatcher.dart
@@ -1,6 +1,5 @@
-import 'package:flutter/material.dart';
-
import 'package:beamer/src/beamer_delegate.dart';
+import 'package:flutter/material.dart';
/// Overrides default back button behavior in [RootBackButtonDispatcher]
/// to do custom [onBack] or [BeamerDelegate.beamBack].
diff --git a/package/lib/src/beamer_delegate.dart b/package/lib/src/beamer_delegate.dart
index c6a4140..68fdb4f 100644
--- a/package/lib/src/beamer_delegate.dart
+++ b/package/lib/src/beamer_delegate.dart
@@ -1,3 +1,5 @@
+import 'dart:async';
+
import 'package:beamer/beamer.dart';
import 'package:beamer/src/browser_tab_title_util_non_web.dart'
if (dart.library.html) 'package:beamer/src/browser_tab_title_util_web.dart'
@@ -40,7 +42,9 @@ class BeamerDelegate extends RouterDelegate
this.updateFromParent = true,
this.updateParent = true,
this.clearBeamingHistoryOn = const {},
+ List? interceptors,
}) {
+ this.interceptors = interceptors ?? [];
_currentBeamParameters = BeamParameters(
transitionDelegate: transitionDelegate,
);
@@ -216,6 +220,8 @@ class BeamerDelegate extends RouterDelegate
/// and stack of pages will be updated as is configured in [BeamGuard].
final List guards;
+ late final List interceptors;
+
/// The list of observers for the [Navigator].
final List navigatorObservers;
@@ -432,13 +438,22 @@ class BeamerDelegate extends RouterDelegate
// run guards on _beamStackCandidate
final context = _context;
if (context != null) {
- final didApply = _runGuards(context, _beamStackCandidate);
+ final didGuardsBlock = _runGuards(context, _beamStackCandidate);
+
_didRunGuards = true;
- if (didApply) {
+
+ if (didGuardsBlock) {
return;
} else {
// TODO revert configuration if guard just blocked navigation
}
+
+ // run interceptors on _beamStackCandidate
+ final isIntercepted = _isIntercepted(context, _beamStackCandidate);
+
+ if (isIntercepted) {
+ return;
+ }
}
// adds the candidate to history
@@ -758,7 +773,7 @@ class BeamerDelegate extends RouterDelegate
_context = context;
if (!_didRunGuards) {
- _runGuards(_context!, _beamStackCandidate);
+ _runGuards(context, _beamStackCandidate);
_addToBeamingHistory(_beamStackCandidate);
}
if (!_initialConfigurationReady && active && parent != null) {
@@ -848,7 +863,12 @@ class BeamerDelegate extends RouterDelegate
}
bool _runGuards(BuildContext context, BeamStack targetBeamStack) {
- final allGuards = (parent?.guards ?? []) + guards + targetBeamStack.guards;
+ final allGuards = [
+ ...?parent?.guards,
+ ...guards,
+ ...targetBeamStack.guards
+ ];
+
for (final guard in allGuards) {
if (guard.shouldGuard(targetBeamStack)) {
final wasApplied = guard.apply(
@@ -869,6 +889,29 @@ class BeamerDelegate extends RouterDelegate
return false;
}
+ bool _isIntercepted(BuildContext context, BeamStack targetBeamStack) {
+ final allInterceptors = [...?parent?.interceptors, ...interceptors];
+
+ for (var i = 0; i < allInterceptors.length; i++) {
+ final interceptor = allInterceptors[i];
+ final isIntercepted = interceptor.enabled &&
+ interceptor.intercept(
+ context,
+ this,
+ _currentPages,
+ currentBeamStack,
+ targetBeamStack,
+ _deepLink,
+ );
+
+ // If any interceptor was intercepted, return true
+ if (isIntercepted) {
+ return true;
+ }
+ }
+ return false;
+ }
+
void _initBeamStack(BeamStack beamStack) {
beamStack.initState();
beamStack.onUpdate();
@@ -1072,6 +1115,20 @@ class BeamerDelegate extends RouterDelegate
}
}
+ FutureOr addInterceptor(BeamInterceptor interceptor) async {
+ if (interceptors.contains(interceptor)) return;
+
+ interceptors.add(interceptor);
+ }
+
+ FutureOr removeInterceptor(BeamInterceptor interceptor) async {
+ if (interceptors.contains(interceptor) == false) return;
+
+ interceptors.remove(interceptor);
+ }
+
+ void removeAllInterceptors() => interceptors.clear();
+
void _update() => update();
// Updates only if it can handle the configuration
diff --git a/package/lib/src/transition_delegates.dart b/package/lib/src/transition_delegates.dart
index fefd162..ec9b193 100644
--- a/package/lib/src/transition_delegates.dart
+++ b/package/lib/src/transition_delegates.dart
@@ -29,7 +29,9 @@ class NoAnimationTransitionDelegate extends TransitionDelegate {
final pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
if (pagelessRoutes != null) {
for (final pagelessRoute in pagelessRoutes) {
- if (pagelessRoute.isWaitingForExitingDecision) pagelessRoute.markForRemove();
+ if (pagelessRoute.isWaitingForExitingDecision) {
+ pagelessRoute.markForRemove();
+ }
}
}
}
diff --git a/package/lib/src/utils.dart b/package/lib/src/utils.dart
index 7fbbf86..454e0f0 100644
--- a/package/lib/src/utils.dart
+++ b/package/lib/src/utils.dart
@@ -19,7 +19,7 @@ abstract class Utils {
BeamParameters? beamParameters,
}) {
for (final beamStack in beamStacks) {
- if (canBeamStackHandleUri(beamStack, uri)) {
+ if (beamStack.canHandle(uri)) {
final routeInformation = RouteInformation(uri: uri, state: routeState);
if (!beamStack.isCurrent) {
beamStack.create(routeInformation, beamParameters);
@@ -32,161 +32,87 @@ abstract class Utils {
return NotFound(path: uri.path);
}
- /// Can a [beamStack], depending on its `pathPatterns`, handle the [uri].
+ /// Creates a state for [BeamStack] based on incoming [uri].
///
- /// Used in [BeamStack.canHandle] and [chooseBeamStack].
- static bool canBeamStackHandleUri(BeamStack beamStack, Uri uri) {
- for (final pathPattern in beamStack.pathPatterns) {
- if (pathPattern is String) {
- // If it is an exact match or asterisk pattern
- if (pathPattern == uri.path ||
- pathPattern == '/*' ||
- pathPattern == '*') {
- return true;
+ /// Used in [BeamState.copyForStack].
+ static BeamState createBeamState(
+ Uri uri, {
+ BeamStack? beamStack,
+ Object? routeState,
+ }) {
+ final pathParameters = {};
+
+ for (final pathPattern in [...?beamStack?.pathPatterns]) {
+ if (pathPattern is! String) {
+ final pathPatternRegex = pathPattern.toRegExp;
+ final url = uri.toString();
+
+ if (pathPatternRegex.hasMatch(url)) {
+ final matches = pathPatternRegex.allMatches(url);
+
+ for (final match in matches) {
+ for (final groupName in match.groupNames) {
+ pathParameters[groupName] = match.namedGroup(groupName) ?? '';
+ }
+ }
+ }
+ } else {
+ if (pathPattern == uri.path || pathPattern == '/*') {
+ return BeamState(
+ pathPatternSegments: uri.pathSegments,
+ queryParameters: uri.queryParameters,
+ routeState: routeState,
+ );
}
- // Clean URI path segments
final uriPathSegments = uri.pathSegments.toList();
if (uriPathSegments.length > 1 && uriPathSegments.last == '') {
uriPathSegments.removeLast();
}
- final pathPatternSegments = Uri.parse(pathPattern).pathSegments;
+ final beamStackPathBlueprintSegments =
+ Uri.parse(pathPattern).pathSegments;
+ var pathSegments = [];
- // If we're in strict mode and URI has fewer segments than pattern,
- // we don't have a match so can continue.
- if (beamStack.strictPathPatterns &&
- uriPathSegments.length < pathPatternSegments.length) {
- continue;
- }
-
- // If URI has more segments and pattern doesn't end with asterisk,
- // we don't have a match so can continue.
- if (uriPathSegments.length > pathPatternSegments.length &&
- (pathPatternSegments.isEmpty ||
- !pathPatternSegments.last.endsWith('*'))) {
- continue;
- }
+ if (uriPathSegments.length > beamStackPathBlueprintSegments.length &&
+ !beamStackPathBlueprintSegments.contains('*')) continue;
var checksPassed = true;
- // Iterating through URI segments
+
for (var i = 0; i < uriPathSegments.length; i++) {
- // If all checks have passed up to i,
- // if pattern has no more segments to traverse and it ended with asterisk,
- // it is a match and we can break,
- if (pathPatternSegments.length < i + 1 &&
- pathPatternSegments.last.endsWith('*')) {
+ if (beamStackPathBlueprintSegments[i] == '*') {
+ pathSegments = uriPathSegments.toList();
checksPassed = true;
break;
}
- // If pattern has asterisk at i-th position,
- // anything matches and we can continue.
- if (pathPatternSegments[i] == '*') {
- continue;
- }
- // If they are not the same and pattern doesn't expects path parameter,
- // there's no match and we can break.
- if (uriPathSegments[i] != pathPatternSegments[i] &&
- !pathPatternSegments[i].startsWith(':')) {
+ if (uriPathSegments[i] != beamStackPathBlueprintSegments[i] &&
+ beamStackPathBlueprintSegments[i][0] != ':') {
checksPassed = false;
break;
+ } else if (beamStackPathBlueprintSegments[i][0] == ':') {
+ pathParameters[beamStackPathBlueprintSegments[i].substring(1)] =
+ uriPathSegments[i];
+ pathSegments.add(beamStackPathBlueprintSegments[i]);
+ } else {
+ pathSegments.add(uriPathSegments[i]);
}
}
- // If no check failed, beamStack can handle this URI.
if (checksPassed) {
- return true;
+ return BeamState(
+ pathPatternSegments: pathSegments,
+ pathParameters: pathParameters,
+ queryParameters: uri.queryParameters,
+ routeState: routeState,
+ );
}
- } else {
- final regexp = tryCastToRegExp(pathPattern);
- return regexp.hasMatch(uri.toString());
}
}
- return false;
- }
- /// Creates a state for [BeamStack] based on incoming [uri].
- ///
- /// Used in [BeamState.copyForStack].
- static BeamState createBeamState(
- Uri uri, {
- BeamStack? beamStack,
- Object? routeState,
- }) {
- if (beamStack != null) {
- // TODO: abstract this and reuse in canBeamStackHandleUri
- for (final pathBlueprint in beamStack.pathPatterns) {
- if (pathBlueprint is String) {
- if (pathBlueprint == uri.path || pathBlueprint == '/*') {
- BeamState(
- pathPatternSegments: uri.pathSegments,
- queryParameters: uri.queryParameters,
- routeState: routeState,
- );
- }
- final uriPathSegments = uri.pathSegments.toList();
- if (uriPathSegments.length > 1 && uriPathSegments.last == '') {
- uriPathSegments.removeLast();
- }
- final beamStackPathBlueprintSegments =
- Uri.parse(pathBlueprint).pathSegments;
- var pathSegments = [];
- final pathParameters = {};
- if (uriPathSegments.length > beamStackPathBlueprintSegments.length &&
- !beamStackPathBlueprintSegments.contains('*')) {
- continue;
- }
- var checksPassed = true;
- for (var i = 0; i < uriPathSegments.length; i++) {
- if (beamStackPathBlueprintSegments[i] == '*') {
- pathSegments = uriPathSegments.toList();
- checksPassed = true;
- break;
- }
- if (uriPathSegments[i] != beamStackPathBlueprintSegments[i] &&
- beamStackPathBlueprintSegments[i][0] != ':') {
- checksPassed = false;
- break;
- } else if (beamStackPathBlueprintSegments[i][0] == ':') {
- pathParameters[beamStackPathBlueprintSegments[i].substring(1)] =
- uriPathSegments[i];
- pathSegments.add(beamStackPathBlueprintSegments[i]);
- } else {
- pathSegments.add(uriPathSegments[i]);
- }
- }
- if (checksPassed) {
- return BeamState(
- pathPatternSegments: pathSegments,
- pathParameters: pathParameters,
- queryParameters: uri.queryParameters,
- routeState: routeState,
- );
- }
- } else {
- final regexp = tryCastToRegExp(pathBlueprint);
- final pathParameters = {};
- final url = uri.toString();
-
- if (regexp.hasMatch(url)) {
- regexp.allMatches(url).forEach((match) {
- for (final groupName in match.groupNames) {
- pathParameters[groupName] = match.namedGroup(groupName) ?? '';
- }
- });
- return BeamState(
- pathPatternSegments: uri.pathSegments,
- pathParameters: pathParameters,
- queryParameters: uri.queryParameters,
- routeState: routeState,
- );
- }
- }
- }
- }
return BeamState(
pathPatternSegments: uri.pathSegments,
queryParameters: uri.queryParameters,
+ pathParameters: pathParameters,
routeState: routeState,
);
}
@@ -210,26 +136,11 @@ abstract class Utils {
}
return true;
} else {
- final regExpPattern = tryCastToRegExp(pattern);
+ final regExpPattern = pattern.toRegExp;
return regExpPattern.hasMatch(exact.toString());
}
}
- /// Wraps the casting of pathBlueprint to RegExp inside a try-catch
- /// and throws a nice FlutterError.
- static RegExp tryCastToRegExp(Pattern pathBlueprint) {
- try {
- return pathBlueprint as RegExp;
- } on TypeError catch (_) {
- throw FlutterError.fromParts([
- DiagnosticsNode.message('Path blueprint can either be:',
- level: DiagnosticLevel.summary),
- DiagnosticsNode.message('1. String'),
- DiagnosticsNode.message('2. RegExp instance')
- ]);
- }
- }
-
/// Removes the trailing / in an URI path and returns the new URI.
///
/// If there is no trailing /, returns the input URI.
@@ -305,3 +216,21 @@ extension BeamerRouteInformationExtension on RouteInformation {
return uri == other.uri && state == other.state;
}
}
+
+/// Some convenient extension methods on [Pattern].
+extension PatternExtension on Pattern {
+ /// Wraps the casting of this pattern to RegExp inside a try-catch
+ /// and throws a nice FlutterError.
+ RegExp get toRegExp {
+ try {
+ return this as RegExp;
+ } on TypeError catch (_) {
+ throw FlutterError.fromParts([
+ DiagnosticsNode.message('Path blueprint can either be:',
+ level: DiagnosticLevel.summary),
+ DiagnosticsNode.message('1. String'),
+ DiagnosticsNode.message('2. RegExp instance')
+ ]);
+ }
+ }
+}
diff --git a/package/test/beaming_history_test.dart b/package/test/beaming_history_test.dart
index 67fc020..0bd2fc8 100644
--- a/package/test/beaming_history_test.dart
+++ b/package/test/beaming_history_test.dart
@@ -1,6 +1,7 @@
import 'package:beamer/beamer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
+
import 'test_stacks.dart';
class StackA extends BeamStack {
diff --git a/package/test/utils_test.dart b/package/test/utils_test.dart
index 9ead37f..788b8aa 100644
--- a/package/test/utils_test.dart
+++ b/package/test/utils_test.dart
@@ -119,7 +119,7 @@ void main() {
test('tryCastToRegExp throws', () {
expect(
- () => Utils.tryCastToRegExp('not-regexp'),
+ () => 'not-regexp'.toRegExp,
throwsA(isA()),
);
});