[go_router] Go router v5 (#2612)
diff --git a/.cirrus.yml b/.cirrus.yml
index abdb234..fbd0689 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -76,7 +76,7 @@
# Run analysis with path-based dependencies to ensure that publishing
# the changes won't break analysis of other packages in the respository
# that depend on it.
- - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates
+ - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates
# This uses --run-on-dirty-packages rather than --packages-for-branch
# since only the packages changed by 'make-deps-path-based' need to be
# checked.
@@ -237,7 +237,7 @@
<< : *FLUTTER_UPGRADE_TEMPLATE
<< : *MACOS_TEMPLATE
matrix:
- ### iOS tasks ###
+ ### iOS tasks ###
- name: ios-platform_tests
env:
PATH: $PATH:/usr/local/bin
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 1746ece..6f2eea2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,17 @@
+## 5.0.0
+
+- Fixes a bug where intermediate route redirect methods are not called.
+- GoRouter implements the RouterConfig interface, allowing you to call
+ MaterialApp.router(routerConfig: _myGoRouter) instead of passing
+ the RouterDelegate, RouteInformationParser, and RouteInformationProvider
+ fields.
+- **BREAKING CHANGE**
+ - Redesigns redirection API, adds asynchronous feature, and adds build context to redirect.
+ - Removes GoRouterRefreshStream
+ - Removes navigatorBuilder
+ - Removes urlPathStrategy
+- [go_router v5 migration guide](https://flutter.dev/go/go-router-v5-breaking-changes)
+
## 4.5.1
- Fixes an issue where GoRoutes with only a redirect were disallowed
diff --git a/packages/go_router/README.md b/packages/go_router/README.md
index 5f9a36d..66dac9f 100644
--- a/packages/go_router/README.md
+++ b/packages/go_router/README.md
@@ -24,9 +24,7 @@
@override
Widget build(BuildContext context) {
return MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: 'GoRouter Example',
);
}
@@ -87,7 +85,7 @@
[pageBuilder](https://pub.dev/documentation/go_router/latest/go_router/GoRoute/pageBuilder.html)
for custom `Page` class.
-## Initalization
+## Initialization
Create a [GoRouter](https://pub.dev/documentation/go_router/latest/go_router/GoRouter-class.html)
object and initialize your `MaterialApp` or `CupertinoApp`:
@@ -100,9 +98,7 @@
);
MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
);
```
@@ -119,6 +115,42 @@
);
```
+## Redirection
+
+You can use redirection to prevent the user from visiting a specific page. In
+go_router, redirection can be asynchronous.
+
+```dart
+GoRouter(
+ ...
+ redirect: (context, state) async {
+ if (await LoginService.of(context).isLoggedIn) {
+ return state.location;
+ }
+ return '/login';
+ },
+);
+```
+
+If the code depends on [BuildContext](https://api.flutter.dev/flutter/widgets/BuildContext-class.html)
+through the [dependOnInheritedWidgetOfExactType](https://api.flutter.dev/flutter/widgets/BuildContext/dependOnInheritedWidgetOfExactType.html)
+(which is how `of` methods are usually implemented), the redirect will be called every time the [InheritedWidget](https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html)
+updated.
+
+### Top-level redirect
+
+The [GoRouter.redirect](https://pub.dev/documentation/go_router/latest/go_router/GoRouter-class.html)
+is always called for every navigation regardless of which GoRoute was matched. The
+top-level redirect always takes priority over route-level redirect.
+
+### Route-level redirect
+
+If the top-level redirect does not redirect to a different location,
+the [GoRoute.redirect](https://pub.dev/documentation/go_router/latest/go_router/GoRoute/redirect.html)
+is then called if the route has matched the GoRoute. If there are multiple
+GoRoute matches, e.g. GoRoute with sub-routes, the parent route redirect takes
+priority over sub-routes' redirect.
+
## Navigation
To navigate between routes, use the [GoRouter.go](https://pub.dev/documentation/go_router/latest/go_router/GoRouter/go.html) method:
@@ -186,6 +218,7 @@
- [Migrating to 2.5](https://flutter.dev/go/go-router-v2-5-breaking-changes)
- [Migrating to 3.0](https://flutter.dev/go/go-router-v3-breaking-changes)
- [Migrating to 4.0](https://flutter.dev/go/go-router-v4-breaking-changes)
+- [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes)
## Changelog
diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md
index 21f6e80..d6ffa67 100644
--- a/packages/go_router/example/README.md
+++ b/packages/go_router/example/README.md
@@ -23,9 +23,14 @@
## [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart)
`flutter run lib/redirection.dart`
-An example to demonstrate how to use redirect to handle a sign-in flow.
+An example to demonstrate how to use redirect to handle a synchronous sign-in flow.
-## [books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books)
+## [Asynchronous Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart)
+`flutter run lib/async_redirection.dart`
+
+An example to demonstrate how to use handle a sign-in flow with a stream authentication service.
+
+## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books)
`flutter run lib/books/main.dart`
A fully fledged example that showcases various go_router APIs.
\ No newline at end of file
diff --git a/packages/go_router/example/lib/async_redirection.dart b/packages/go_router/example/lib/async_redirection.dart
new file mode 100644
index 0000000..918fada
--- /dev/null
+++ b/packages/go_router/example/lib/async_redirection.dart
@@ -0,0 +1,248 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+// This scenario demonstrates how to use redirect to handle a asynchronous
+// sign-in flow.
+//
+// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an
+// InheritedNotifier, StreamAuthScope, and relies on
+// `dependOnInheritedWidgetOfExactType` to create a dependency between the
+// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new
+// event, the dependency will cause the go_router to reparse the current url
+// which will also trigger the redirect.
+
+void main() => runApp(StreamAuthScope(child: App()));
+
+/// The main app.
+class App extends StatelessWidget {
+ /// Creates an [App].
+ App({Key? key}) : super(key: key);
+
+ /// The title of the app.
+ static const String title = 'GoRouter Example: Redirection';
+
+ // add the login info into the tree as app state that can change over time
+ @override
+ Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
+ routeInformationParser: _router.routeInformationParser,
+ routerDelegate: _router.routerDelegate,
+ title: title,
+ debugShowCheckedModeBanner: false,
+ );
+
+ late final GoRouter _router = GoRouter(
+ routes: <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) =>
+ const HomeScreen(),
+ ),
+ GoRoute(
+ path: '/login',
+ builder: (BuildContext context, GoRouterState state) =>
+ const LoginScreen(),
+ ),
+ ],
+
+ // redirect to the login page if the user is not logged in
+ redirect: (BuildContext context, GoRouterState state) async {
+ // Using `of` method creates a dependency of StreamAuthScope. It will
+ // cause go_router to reparse current route if StreamAuth has new sign-in
+ // information.
+ final bool loggedIn = await StreamAuthScope.of(context).isSignedIn();
+ final bool loggingIn = state.subloc == '/login';
+ if (!loggedIn) {
+ return loggingIn ? null : '/login';
+ }
+
+ // if the user is logged in but still on the login page, send them to
+ // the home page
+ if (loggingIn) {
+ return '/';
+ }
+
+ // no need to redirect at all
+ return null;
+ },
+ );
+}
+
+/// The login screen.
+class LoginScreen extends StatefulWidget {
+ /// Creates a [LoginScreen].
+ const LoginScreen({Key? key}) : super(key: key);
+
+ @override
+ State<LoginScreen> createState() => _LoginScreenState();
+}
+
+class _LoginScreenState extends State<LoginScreen>
+ with TickerProviderStateMixin {
+ bool loggingIn = false;
+ late final AnimationController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = AnimationController(
+ vsync: this,
+ duration: const Duration(seconds: 1),
+ )..addListener(() {
+ setState(() {});
+ });
+ controller.repeat();
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => Scaffold(
+ appBar: AppBar(title: const Text(App.title)),
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ if (loggingIn) CircularProgressIndicator(value: controller.value),
+ if (!loggingIn)
+ ElevatedButton(
+ onPressed: () {
+ StreamAuthScope.of(context).signIn('test-user');
+ setState(() {
+ loggingIn = true;
+ });
+ },
+ child: const Text('Login'),
+ ),
+ ],
+ ),
+ ),
+ );
+}
+
+/// The home screen.
+class HomeScreen extends StatelessWidget {
+ /// Creates a [HomeScreen].
+ const HomeScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final StreamAuth info = StreamAuthScope.of(context);
+
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text(App.title),
+ actions: <Widget>[
+ IconButton(
+ onPressed: () => info.signOut(),
+ tooltip: 'Logout: ${info.currentUser}',
+ icon: const Icon(Icons.logout),
+ )
+ ],
+ ),
+ body: const Center(
+ child: Text('HomeScreen'),
+ ),
+ );
+ }
+}
+
+/// A scope that provides [StreamAuth] for the subtree.
+class StreamAuthScope extends InheritedNotifier<StreamAuthNotifier> {
+ /// Creates a [StreamAuthScope] sign in scope.
+ StreamAuthScope({
+ Key? key,
+ required Widget child,
+ }) : super(
+ key: key,
+ notifier: StreamAuthNotifier(),
+ child: child,
+ );
+
+ /// Gets the [StreamAuth].
+ static StreamAuth of(BuildContext context) {
+ return context
+ .dependOnInheritedWidgetOfExactType<StreamAuthScope>()!
+ .notifier!
+ .streamAuth;
+ }
+}
+
+/// A class that converts [StreamAuth] into a [ChangeNotifier].
+class StreamAuthNotifier extends ChangeNotifier {
+ /// Creates a [StreamAuthNotifier].
+ StreamAuthNotifier() : streamAuth = StreamAuth() {
+ streamAuth.onCurrentUserChanged.listen((String? string) {
+ notifyListeners();
+ });
+ }
+
+ /// The stream auth client.
+ final StreamAuth streamAuth;
+}
+
+/// An asynchronous log in services mock with stream similar to google_sign_in.
+///
+/// This class adds an artificial delay of 3 second when logging in an user, and
+/// will automatically clear the login session after [refreshInterval].
+class StreamAuth {
+ /// Creates an [StreamAuth] that clear the current user session in
+ /// [refeshInterval] second.
+ StreamAuth({this.refreshInterval = 20})
+ : _userStreamController = StreamController<String?>.broadcast() {
+ _userStreamController.stream.listen((String? currentUser) {
+ _currentUser = currentUser;
+ });
+ }
+
+ /// The current user.
+ String? get currentUser => _currentUser;
+ String? _currentUser;
+
+ /// Checks whether current user is signed in with an artificial delay to mimic
+ /// async operation.
+ Future<bool> isSignedIn() async {
+ await Future<void>.delayed(const Duration(seconds: 1));
+ return _currentUser != null;
+ }
+
+ /// A stream that notifies when current user has changed.
+ Stream<String?> get onCurrentUserChanged => _userStreamController.stream;
+ final StreamController<String?> _userStreamController;
+
+ /// The interval that automatically signs out the user.
+ final int refreshInterval;
+
+ Timer? _timer;
+ Timer _createRefreshTimer() {
+ return Timer(Duration(seconds: refreshInterval), () {
+ _userStreamController.add(null);
+ _timer = null;
+ });
+ }
+
+ /// Signs in a user with an artificial delay to mimic async operation.
+ Future<void> signIn(String newUserName) async {
+ await Future<void>.delayed(const Duration(seconds: 3));
+ _userStreamController.add(newUserName);
+ _timer?.cancel();
+ _timer = _createRefreshTimer();
+ }
+
+ /// Signs out the current user.
+ Future<void> signOut() async {
+ _timer?.cancel();
+ _timer = null;
+ _userStreamController.add(null);
+ }
+}
diff --git a/packages/go_router/example/lib/books/main.dart b/packages/go_router/example/lib/books/main.dart
index 241c368..7ee8820 100644
--- a/packages/go_router/example/lib/books/main.dart
+++ b/packages/go_router/example/lib/books/main.dart
@@ -43,7 +43,7 @@
routes: <GoRoute>[
GoRoute(
path: '/',
- redirect: (_) => '/books',
+ redirect: (_, __) => '/books',
),
GoRoute(
path: '/signin',
@@ -60,11 +60,11 @@
),
GoRoute(
path: '/books',
- redirect: (_) => '/books/popular',
+ redirect: (_, __) => '/books/popular',
),
GoRoute(
path: '/book/:bookId',
- redirect: (GoRouterState state) =>
+ redirect: (BuildContext context, GoRouterState state) =>
'/books/all/${state.params['bookId']}',
),
GoRoute(
@@ -92,7 +92,7 @@
),
GoRoute(
path: '/author/:authorId',
- redirect: (GoRouterState state) =>
+ redirect: (BuildContext context, GoRouterState state) =>
'/authors/${state.params['authorId']}',
),
GoRoute(
@@ -135,7 +135,7 @@
debugLogDiagnostics: true,
);
- String? _guard(GoRouterState state) {
+ String? _guard(BuildContext context, GoRouterState state) {
final bool signedIn = _auth.signedIn;
final bool signingIn = state.subloc == '/signin';
diff --git a/packages/go_router/example/lib/main.dart b/packages/go_router/example/lib/main.dart
index fab5a36..c47fc49 100644
--- a/packages/go_router/example/lib/main.dart
+++ b/packages/go_router/example/lib/main.dart
@@ -26,9 +26,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/named_routes.dart b/packages/go_router/example/lib/named_routes.dart
index adb21e7..237581f 100644
--- a/packages/go_router/example/lib/named_routes.dart
+++ b/packages/go_router/example/lib/named_routes.dart
@@ -61,9 +61,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
debugShowCheckedModeBanner: false,
);
diff --git a/packages/go_router/example/lib/others/error_screen.dart b/packages/go_router/example/lib/others/error_screen.dart
index b1c4fa9..f7edca0 100644
--- a/packages/go_router/example/lib/others/error_screen.dart
+++ b/packages/go_router/example/lib/others/error_screen.dart
@@ -17,9 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/extra_param.dart b/packages/go_router/example/lib/others/extra_param.dart
index 3b6ca0e..a4d40e9 100644
--- a/packages/go_router/example/lib/others/extra_param.dart
+++ b/packages/go_router/example/lib/others/extra_param.dart
@@ -53,9 +53,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/init_loc.dart b/packages/go_router/example/lib/others/init_loc.dart
index 899c3c1..31ca594 100644
--- a/packages/go_router/example/lib/others/init_loc.dart
+++ b/packages/go_router/example/lib/others/init_loc.dart
@@ -17,9 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/nav_builder.dart b/packages/go_router/example/lib/others/nav_builder.dart
deleted file mode 100644
index c3a3bbb..0000000
--- a/packages/go_router/example/lib/others/nav_builder.dart
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
-import 'package:provider/provider.dart';
-
-/// The login information.
-class LoginInfo extends ChangeNotifier {
- /// The username of login.
- String get userName => _userName;
- String _userName = '';
-
- /// Whether a user has logged in.
- bool get loggedIn => _userName.isNotEmpty;
-
- /// Logs in a user.
- void login(String userName) {
- _userName = userName;
- notifyListeners();
- }
-
- /// Logs out the current user.
- void logout() {
- _userName = '';
- notifyListeners();
- }
-}
-
-void main() => runApp(App());
-
-/// The main app.
-class App extends StatelessWidget {
- /// Creates an [App].
- App({Key? key}) : super(key: key);
-
- final LoginInfo _loginInfo = LoginInfo();
-
- /// The title of the app.
- static const String title = 'GoRouter Example: Navigator Builder';
-
- @override
- Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
- title: title,
- );
-
- late final GoRouter _router = GoRouter(
- debugLogDiagnostics: true,
- routes: <GoRoute>[
- GoRoute(
- name: 'home',
- path: '/',
- builder: (BuildContext context, GoRouterState state) =>
- const HomeScreenNoLogout(),
- ),
- GoRoute(
- name: 'login',
- path: '/login',
- builder: (BuildContext context, GoRouterState state) =>
- const LoginScreen(),
- ),
- ],
-
- // changes on the listenable will cause the router to refresh it's route
- refreshListenable: _loginInfo,
-
- // redirect to the login page if the user is not logged in
- redirect: (GoRouterState state) {
- final bool loggedIn = _loginInfo.loggedIn;
- const String loginLocation = '/login';
- final bool loggingIn = state.subloc == loginLocation;
-
- if (!loggedIn) {
- return loggingIn ? null : loginLocation;
- }
- if (loggingIn) {
- return state.namedLocation('home');
- }
- return null;
- },
-
- // add a wrapper around the navigator to:
- // - put loginInfo into the widget tree, and to
- // - add an overlay to show a logout option
- navigatorBuilder:
- (BuildContext context, GoRouterState state, Widget child) =>
- ChangeNotifierProvider<LoginInfo>.value(
- value: _loginInfo,
- builder: (BuildContext context, Widget? _) {
- return _loginInfo.loggedIn ? AuthOverlay(child: child) : child;
- },
- ),
- );
-}
-
-/// A simple class for placing an exit button on top of all screens.
-class AuthOverlay extends StatelessWidget {
- /// Creates an [AuthOverlay].
- const AuthOverlay({required this.child, Key? key}) : super(key: key);
-
- /// The child subtree.
- final Widget child;
-
- @override
- Widget build(BuildContext context) => Stack(
- children: <Widget>[
- child,
- Positioned(
- top: 90,
- right: 4,
- child: ElevatedButton(
- onPressed: () {
- context.read<LoginInfo>().logout();
- context.goNamed('home'); // clear out the `from` query param
- },
- child: const Icon(Icons.logout),
- ),
- ),
- ],
- );
-}
-
-/// The home screen without a logout button.
-class HomeScreenNoLogout extends StatelessWidget {
- /// Creates a [HomeScreenNoLogout].
- const HomeScreenNoLogout({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) => Scaffold(
- appBar: AppBar(title: const Text(App.title)),
- body: const Center(
- child: Text('home screen'),
- ),
- );
-}
-
-/// The login screen.
-class LoginScreen extends StatelessWidget {
- /// Creates a [LoginScreen].
- const LoginScreen({Key? key}) : super(key: key);
-
- @override
- Widget build(BuildContext context) => Scaffold(
- appBar: AppBar(title: const Text(App.title)),
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- ElevatedButton(
- onPressed: () {
- // log a user in, letting all the listeners know
- context.read<LoginInfo>().login('test-user');
- },
- child: const Text('Login'),
- ),
- ],
- ),
- ),
- );
-}
diff --git a/packages/go_router/example/lib/others/nav_observer.dart b/packages/go_router/example/lib/others/nav_observer.dart
index 5afd016..a63023d 100644
--- a/packages/go_router/example/lib/others/nav_observer.dart
+++ b/packages/go_router/example/lib/others/nav_observer.dart
@@ -18,9 +18,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/push.dart b/packages/go_router/example/lib/others/push.dart
index 1288f12..5d54ec2 100644
--- a/packages/go_router/example/lib/others/push.dart
+++ b/packages/go_router/example/lib/others/push.dart
@@ -17,9 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/router_neglect.dart b/packages/go_router/example/lib/others/router_neglect.dart
index 4f4acfb..d5c2280 100644
--- a/packages/go_router/example/lib/others/router_neglect.dart
+++ b/packages/go_router/example/lib/others/router_neglect.dart
@@ -17,9 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/lib/others/state_restoration.dart b/packages/go_router/example/lib/others/state_restoration.dart
index 33d64ca..5c6016f 100644
--- a/packages/go_router/example/lib/others/state_restoration.dart
+++ b/packages/go_router/example/lib/others/state_restoration.dart
@@ -32,9 +32,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: App.title,
restorationScopeId: 'app',
);
diff --git a/packages/go_router/example/lib/others/transitions.dart b/packages/go_router/example/lib/others/transitions.dart
index 555e070..51c2a24 100644
--- a/packages/go_router/example/lib/others/transitions.dart
+++ b/packages/go_router/example/lib/others/transitions.dart
@@ -17,9 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
@@ -27,7 +25,7 @@
routes: <GoRoute>[
GoRoute(
path: '/',
- redirect: (_) => '/none',
+ redirect: (_, __) => '/none',
),
GoRoute(
path: '/fade',
diff --git a/packages/go_router/example/lib/path_and_query_parameters.dart b/packages/go_router/example/lib/path_and_query_parameters.dart
index fe9b8ba..7e52baf 100755
--- a/packages/go_router/example/lib/path_and_query_parameters.dart
+++ b/packages/go_router/example/lib/path_and_query_parameters.dart
@@ -62,9 +62,7 @@
// add the login info into the tree as app state that can change over time
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
debugShowCheckedModeBanner: false,
);
diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart
index d57b5d4..1e23573 100644
--- a/packages/go_router/example/lib/redirection.dart
+++ b/packages/go_router/example/lib/redirection.dart
@@ -51,9 +51,7 @@
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
debugShowCheckedModeBanner: false,
),
@@ -74,7 +72,7 @@
],
// redirect to the login page if the user is not logged in
- redirect: (GoRouterState state) {
+ redirect: (BuildContext context, GoRouterState state) {
// if the user is not logged in, they need to login
final bool loggedIn = _loginInfo.loggedIn;
final bool loggingIn = state.subloc == '/login';
diff --git a/packages/go_router/example/lib/sub_routes.dart b/packages/go_router/example/lib/sub_routes.dart
index 6e1632a..8bd7728 100644
--- a/packages/go_router/example/lib/sub_routes.dart
+++ b/packages/go_router/example/lib/sub_routes.dart
@@ -29,9 +29,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
- routeInformationProvider: _router.routeInformationProvider,
- routeInformationParser: _router.routeInformationParser,
- routerDelegate: _router.routerDelegate,
+ routerConfig: _router,
title: title,
);
diff --git a/packages/go_router/example/pubspec.yaml b/packages/go_router/example/pubspec.yaml
index abd1598..b218565 100644
--- a/packages/go_router/example/pubspec.yaml
+++ b/packages/go_router/example/pubspec.yaml
@@ -5,7 +5,7 @@
environment:
sdk: ">=2.14.0 <3.0.0"
- flutter: ">=2.10.0"
+ flutter: ">=3.3.0"
dependencies:
adaptive_dialog: ^1.2.0
diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart
index d47c63d..1d9f9dd 100644
--- a/packages/go_router/lib/go_router.dart
+++ b/packages/go_router/lib/go_router.dart
@@ -10,7 +10,6 @@
show GoRoute, GoRouterState, RouteBase, ShellRoute;
export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
-export 'src/misc/refresh_stream.dart';
export 'src/pages/custom_transition_page.dart';
export 'src/platform.dart' show UrlPathStrategy;
export 'src/route_data.dart' show GoRouteData, TypedGoRoute;
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index 41cbc3e..6170984 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -54,6 +54,11 @@
VoidCallback pop,
bool routerNeglect,
) {
+ if (matchList.isEmpty) {
+ // The build method can be called before async redirect finishes. Build a
+ // empty box until then.
+ return const SizedBox.shrink();
+ }
try {
return tryBuild(
context, matchList, pop, routerNeglect, configuration.navigatorKey);
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 06abcc2..f2ee938 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -173,6 +173,7 @@
@override
Future<void> setNewRoutePath(RouteMatchList configuration) {
_matchList = configuration;
+ assert(_matchList.isNotEmpty);
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null);
diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart
index 3011d13..207ec45 100644
--- a/packages/go_router/lib/src/information_provider.dart
+++ b/packages/go_router/lib/src/information_provider.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'parser.dart';
@@ -96,18 +97,17 @@
}
@override
- Future<bool> didPushRouteInformation(
- RouteInformation routeInformation) async {
+ Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
assert(hasListeners);
_platformReportsNewRouteInformation(routeInformation);
- return true;
+ return SynchronousFuture<bool>(true);
}
@override
- Future<bool> didPushRoute(String route) async {
+ Future<bool> didPushRoute(String route) {
assert(hasListeners);
_platformReportsNewRouteInformation(RouteInformation(location: route));
- return true;
+ return SynchronousFuture<bool>(true);
}
}
@@ -115,5 +115,5 @@
/// in use with the [GoRouteInformationParser].
class DebugGoRouteInformation extends RouteInformation {
/// Creates a [DebugGoRouteInformation].
- DebugGoRouteInformation({super.location, super.state});
+ const DebugGoRouteInformation({super.location, super.state});
}
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index 060bd3b..1775c71 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:flutter/widgets.dart';
+
import 'configuration.dart';
import 'match.dart';
import 'path_utils.dart';
@@ -83,7 +85,7 @@
}
/// An optional object provided by the app during navigation.
- Object? get extra => _matches.last.extra;
+ Object? get extra => _matches.isEmpty ? null : _matches.last.extra;
/// The last matching route.
RouteMatch get last => _matches.last;
@@ -220,3 +222,25 @@
// consider adding the dynamic route at the end of the routes
return result.first;
}
+
+/// The match used when there is an error during parsing.
+RouteMatchList errorScreen(Uri uri, String errorMessage) {
+ final Exception error = Exception(errorMessage);
+ return RouteMatchList(<RouteMatch>[
+ RouteMatch(
+ subloc: uri.path,
+ fullpath: uri.path,
+ encodedParams: <String, String>{},
+ queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
+ extra: null,
+ error: error,
+ route: GoRoute(
+ path: uri.toString(),
+ pageBuilder: (BuildContext context, GoRouterState state) {
+ throw UnimplementedError();
+ },
+ ),
+ ),
+ ]);
+}
diff --git a/packages/go_router/lib/src/misc/refresh_stream.dart b/packages/go_router/lib/src/misc/refresh_stream.dart
deleted file mode 100644
index 01acb43..0000000
--- a/packages/go_router/lib/src/misc/refresh_stream.dart
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-
-/// Converts a [Stream] into a [Listenable]
-///
-/// {@tool snippet}
-/// Typical usage is as follows:
-///
-/// ```dart
-/// GoRouter(
-/// refreshListenable: GoRouterRefreshStream(stream),
-/// );
-/// ```
-/// {@end-tool}
-class GoRouterRefreshStream extends ChangeNotifier {
- /// Creates a [GoRouterRefreshStream].
- ///
- /// Every time the [stream] receives an event the [GoRouter] will refresh its
- /// current route.
- GoRouterRefreshStream(Stream<dynamic> stream) {
- notifyListeners();
- _subscription = stream.asBroadcastStream().listen(
- (dynamic _) => notifyListeners(),
- );
- }
-
- late final StreamSubscription<dynamic> _subscription;
-
- @override
- void dispose() {
- _subscription.cancel();
- super.dispose();
- }
-}
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 9959553..7c7d1de 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -2,13 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'information_provider.dart';
import 'logging.dart';
-import 'match.dart';
import 'matching.dart';
import 'redirection.dart';
@@ -40,10 +41,17 @@
/// Defaults to false.
final bool debugRequireGoRouteInformationProvider;
+ /// The future of current route parsing.
+ ///
+ /// This is used for testing asynchronous redirection.
+ @visibleForTesting
+ Future<RouteMatchList>? debugParserFuture;
+
/// Called by the [Router]. The
@override
- Future<RouteMatchList> parseRouteInformation(
+ Future<RouteMatchList> parseRouteInformationWithDependencies(
RouteInformation routeInformation,
+ BuildContext context,
) {
assert(() {
if (debugRequireGoRouteInformationProvider) {
@@ -56,43 +64,46 @@
}
return true;
}());
+ late final RouteMatchList initialMatches;
try {
- late final RouteMatchList initialMatches;
- try {
- initialMatches = matcher.findMatch(routeInformation.location!,
- extra: routeInformation.state);
- } on MatcherError {
- log.info('No initial matches: ${routeInformation.location}');
-
- // If there is a matching error for the initial location, we should
- // still try to process the top-level redirects.
- initialMatches = RouteMatchList.empty();
- }
- final RouteMatchList matches = redirector(
- initialMatches, configuration, matcher,
+ initialMatches = matcher.findMatch(routeInformation.location!,
extra: routeInformation.state);
+ } on MatcherError {
+ log.info('No initial matches: ${routeInformation.location}');
+
+ // If there is a matching error for the initial location, we should
+ // still try to process the top-level redirects.
+ initialMatches = RouteMatchList.empty();
+ }
+ Future<RouteMatchList> processRedirectorResult(RouteMatchList matches) {
if (matches.isEmpty) {
- return SynchronousFuture<RouteMatchList>(_errorScreen(
+ return SynchronousFuture<RouteMatchList>(errorScreen(
Uri.parse(routeInformation.location!),
MatcherError('no routes for location', routeInformation.location!)
.toString()));
}
-
- // Use [SynchronousFuture] so that the initial url is processed
- // synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<RouteMatchList>(matches);
- } on RedirectionError catch (e) {
- log.info('Redirection error: ${e.message}');
- final Uri uri = e.location;
- return SynchronousFuture<RouteMatchList>(_errorScreen(uri, e.message));
- } on MatcherError catch (e) {
- // The RouteRedirector uses the matcher to find the match, so a match
- // exception can happen during redirection. For example, the redirector
- // redirects from `/a` to `/b`, it needs to get the matches for `/b`.
- log.info('Match error: ${e.message}');
- final Uri uri = Uri.parse(e.location);
- return SynchronousFuture<RouteMatchList>(_errorScreen(uri, e.message));
}
+
+ final FutureOr<RouteMatchList> redirectorResult = redirector(
+ context,
+ SynchronousFuture<RouteMatchList>(initialMatches),
+ configuration,
+ matcher,
+ extra: routeInformation.state,
+ );
+ if (redirectorResult is RouteMatchList) {
+ return processRedirectorResult(redirectorResult);
+ }
+
+ return debugParserFuture = redirectorResult.then(processRedirectorResult);
+ }
+
+ @override
+ Future<RouteMatchList> parseRouteInformation(
+ RouteInformation routeInformation) {
+ throw UnimplementedError(
+ 'use parseRouteInformationWithDependencies instead');
}
/// for use by the Router architecture as part of the RouteInformationParser
@@ -103,26 +114,4 @@
state: configuration.extra,
);
}
-
- /// Creates a match that routes to the error page.
- RouteMatchList _errorScreen(Uri uri, String errorMessage) {
- final Exception error = Exception(errorMessage);
- return RouteMatchList(<RouteMatch>[
- RouteMatch(
- subloc: uri.path,
- fullpath: uri.path,
- encodedParams: <String, String>{},
- queryParams: uri.queryParameters,
- queryParametersAll: uri.queryParametersAll,
- extra: null,
- error: error,
- route: GoRoute(
- path: uri.toString(),
- pageBuilder: (BuildContext context, GoRouterState state) {
- throw UnimplementedError();
- },
- ),
- ),
- ]);
- }
}
diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart
index be09bb2..9f68716 100644
--- a/packages/go_router/lib/src/redirection.dart
+++ b/packages/go_router/lib/src/redirection.dart
@@ -2,39 +2,107 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
+import 'package:flutter/cupertino.dart';
+
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
-import 'typedefs.dart';
/// A GoRouter redirector function.
-// TODO(johnpryan): make redirector async
-// See https://github.com/flutter/flutter/issues/105808
-typedef RouteRedirector = RouteMatchList Function(RouteMatchList matches,
- RouteConfiguration configuration, RouteMatcher matcher,
- {Object? extra});
+typedef RouteRedirector = FutureOr<RouteMatchList> Function(
+ BuildContext, FutureOr<RouteMatchList>, RouteConfiguration, RouteMatcher,
+ {List<RouteMatchList>? redirectHistory, Object? extra});
/// Processes redirects by returning a new [RouteMatchList] representing the new
/// location.
-RouteMatchList redirect(RouteMatchList prevMatchList,
- RouteConfiguration configuration, RouteMatcher matcher,
- {Object? extra}) {
- RouteMatchList matches;
+FutureOr<RouteMatchList> redirect(
+ BuildContext context,
+ FutureOr<RouteMatchList> prevMatchListFuture,
+ RouteConfiguration configuration,
+ RouteMatcher matcher,
+ {List<RouteMatchList>? redirectHistory,
+ Object? extra}) {
+ FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
+ FutureOr<RouteMatchList> processTopLevelRedirect(
+ String? topRedirectLocation) {
+ if (topRedirectLocation != null) {
+ final RouteMatchList newMatch = _getNewMatches(
+ topRedirectLocation,
+ prevMatchList.location,
+ configuration,
+ matcher,
+ redirectHistory!,
+ );
+ if (newMatch.isError) {
+ return newMatch;
+ }
+ return redirect(
+ context,
+ newMatch,
+ configuration,
+ matcher,
+ redirectHistory: redirectHistory,
+ extra: extra,
+ );
+ }
- // Store each redirect to detect loops
- final List<RouteMatchList> redirects = <RouteMatchList>[prevMatchList];
+ // Merge new params to keep params from previously matched paths, e.g.
+ // /users/:userId/book/:bookId provides userId and bookId to bookgit /:bookId
+ Map<String, String> previouslyMatchedParams = <String, String>{};
+ for (final RouteMatch match in prevMatchList.matches) {
+ assert(
+ !previouslyMatchedParams.keys.any(match.encodedParams.containsKey),
+ 'Duplicated parameter names',
+ );
+ match.encodedParams.addAll(previouslyMatchedParams);
+ previouslyMatchedParams = match.encodedParams;
+ }
+ FutureOr<RouteMatchList> processRouteLevelRedirect(
+ String? routeRedirectLocation) {
+ if (routeRedirectLocation != null) {
+ final RouteMatchList newMatch = _getNewMatches(
+ routeRedirectLocation,
+ prevMatchList.location,
+ configuration,
+ matcher,
+ redirectHistory!,
+ );
- // Keep looping until redirecting is done
- while (true) {
- final RouteMatchList currentMatches = redirects.last;
+ if (newMatch.isError) {
+ return newMatch;
+ }
+ return redirect(
+ context,
+ newMatch,
+ configuration,
+ matcher,
+ redirectHistory: redirectHistory,
+ extra: extra,
+ );
+ }
+ return prevMatchList;
+ }
+ final FutureOr<String?> routeLevelRedirectResult =
+ _getRouteLevelRedirect(context, configuration, prevMatchList, 0);
+ if (routeLevelRedirectResult is String?) {
+ return processRouteLevelRedirect(routeLevelRedirectResult);
+ }
+ return routeLevelRedirectResult
+ .then<RouteMatchList>(processRouteLevelRedirect);
+ }
+
+ redirectHistory ??= <RouteMatchList>[prevMatchList];
// Check for top-level redirect
- final Uri uri = currentMatches.location;
- final String? topRedirectLocation = configuration.topRedirect(
+ final Uri uri = prevMatchList.location;
+ final FutureOr<String?> topRedirectResult = configuration.topRedirect(
+ context,
GoRouterState(
configuration,
- location: currentMatches.location.toString(),
+ location: prevMatchList.location.toString(),
name: null,
// No name available at the top level trim the query params off the
// sub-location to match route.redirect
@@ -45,68 +113,89 @@
),
);
- if (topRedirectLocation != null) {
- final RouteMatchList newMatch = matcher.findMatch(topRedirectLocation);
- _addRedirect(redirects, newMatch, prevMatchList.location,
- configuration.redirectLimit);
- continue;
+ if (topRedirectResult is String?) {
+ return processTopLevelRedirect(topRedirectResult);
}
+ return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
+ }
- // If there's no top-level redirect, keep the matches the same as before.
- matches = currentMatches;
+ if (prevMatchListFuture is RouteMatchList) {
+ return processRedirect(prevMatchListFuture);
+ }
+ return prevMatchListFuture.then<RouteMatchList>(processRedirect);
+}
- // Merge new params to keep params from previously matched paths, e.g.
- // /users/:userId/book/:bookId provides userId and bookId to book/:bookId
- Map<String, String> previouslyMatchedParams = <String, String>{};
- for (final RouteMatch match in currentMatches.matches) {
- assert(
- !previouslyMatchedParams.keys.any(match.encodedParams.containsKey),
- 'Duplicated parameter names',
- );
- match.encodedParams.addAll(previouslyMatchedParams);
- previouslyMatchedParams = match.encodedParams;
- }
-
- // check top route for redirect
- final RouteMatch? top = matches.isNotEmpty ? matches.last : null;
- if (top == null) {
- break;
- }
-
- final RouteBase topRoute = top.route;
- assert(topRoute is GoRoute,
- 'Last RouteMatch should contain a GoRoute, but was ${topRoute.runtimeType}');
- final GoRoute topGoRoute = topRoute as GoRoute;
- final GoRouterRedirect? redirect = topGoRoute.redirect;
- if (redirect == null) {
- break;
- }
-
- final String? topRouteLocation = redirect(
+FutureOr<String?> _getRouteLevelRedirect(
+ BuildContext context,
+ RouteConfiguration configuration,
+ RouteMatchList matchList,
+ int currentCheckIndex,
+) {
+ if (currentCheckIndex >= matchList.matches.length) {
+ return null;
+ }
+ final RouteMatch match = matchList.matches[currentCheckIndex];
+ FutureOr<String?> processRouteRedirect(String? newLocation) =>
+ newLocation ??
+ _getRouteLevelRedirect(
+ context, configuration, matchList, currentCheckIndex + 1);
+ final RouteBase route = match.route;
+ FutureOr<String?> routeRedirectResult;
+ if (route is GoRoute && route.redirect != null) {
+ routeRedirectResult = route.redirect!(
+ context,
GoRouterState(
configuration,
- location: currentMatches.location.toString(),
- subloc: top.subloc,
- name: topGoRoute.name,
- path: topGoRoute.path,
- fullpath: top.fullpath,
- extra: top.extra,
- params: top.decodedParams,
- queryParams: top.queryParams,
- queryParametersAll: top.queryParametersAll,
+ location: matchList.location.toString(),
+ subloc: match.subloc,
+ name: route.name,
+ path: route.path,
+ fullpath: match.fullpath,
+ extra: match.extra,
+ params: match.decodedParams,
+ queryParams: match.queryParams,
+ queryParametersAll: match.queryParametersAll,
),
);
-
- if (topRouteLocation == null) {
- break;
- }
-
- final RouteMatchList newMatchList = matcher.findMatch(topRouteLocation);
- _addRedirect(redirects, newMatchList, prevMatchList.location,
- configuration.redirectLimit);
- continue;
}
- return matches;
+ if (routeRedirectResult is String?) {
+ return processRouteRedirect(routeRedirectResult);
+ }
+ return routeRedirectResult.then<String?>(processRouteRedirect);
+}
+
+RouteMatchList _getNewMatches(
+ String newLocation,
+ Uri previousLocation,
+ RouteConfiguration configuration,
+ RouteMatcher matcher,
+ List<RouteMatchList> redirectHistory,
+) {
+ try {
+ final RouteMatchList newMatch = matcher.findMatch(newLocation);
+ _addRedirect(redirectHistory, newMatch, previousLocation,
+ configuration.redirectLimit);
+ return newMatch;
+ } on RedirectionError catch (e) {
+ return _handleRedirectionError(e);
+ } on MatcherError catch (e) {
+ return _handleMatcherError(e);
+ }
+}
+
+RouteMatchList _handleMatcherError(MatcherError error) {
+ // The RouteRedirector uses the matcher to find the match, so a match
+ // exception can happen during redirection. For example, the redirector
+ // redirects from `/a` to `/b`, it needs to get the matches for `/b`.
+ log.info('Match error: ${error.message}');
+ final Uri uri = Uri.parse(error.location);
+ return errorScreen(uri, error.message);
+}
+
+RouteMatchList _handleRedirectionError(RedirectionError error) {
+ log.info('Redirection error: ${error.message}');
+ final Uri uri = error.location;
+ return errorScreen(uri, error.message);
}
/// A configuration error detected while processing redirects.
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index 98dd5f5..14800d2 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -279,10 +279,38 @@
/// );
/// ```
///
+ /// If there are multiple redirects in the matched routes, the parent route's
+ /// redirect takes priority over sub-route's.
+ ///
+ /// For example:
+ /// ```
+ /// final GoRouter _router = GoRouter(
+ /// routes: <GoRoute>[
+ /// GoRoute(
+ /// path: '/',
+ /// redirect: (_) => '/page1', // this takes priority over the sub-route.
+ /// routes: <GoRoute>[
+ /// GoRoute(
+ /// path: 'child',
+ /// redirect: (_) => '/page2',
+ /// ),
+ /// ],
+ /// ),
+ /// ],
+ /// );
+ /// ```
+ ///
+ /// The `context.go('/child')` will be redirected to `/page1` instead of
+ /// `/page2`.
+ ///
/// Redirect can also be used for conditionally preventing users from visiting
/// routes, also known as route guards. One canonical example is user
/// authentication. See [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart)
/// for a complete runnable example.
+ ///
+ /// If [BuildContext.dependOnInheritedWidgetOfExactType] is used during the
+ /// redirection (which is how `of` method is usually implemented), a
+ /// re-evaluation will be triggered if the [InheritedWidget] changes.
final GoRouterRedirect? redirect;
/// An optional key specifying which Navigator to display this route's screen
diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart
index 4a2d4d4..f3a7f32 100644
--- a/packages/go_router/lib/src/route_data.dart
+++ b/packages/go_router/lib/src/route_data.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
+
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import 'package:meta/meta_meta.dart';
@@ -68,7 +70,7 @@
/// [redirect].
///
/// Corresponds to [GoRoute.redirect].
- String? redirect() => null;
+ FutureOr<String?> redirect() => null;
/// A helper function used by generated code.
///
@@ -108,7 +110,8 @@
Page<void> pageBuilder(BuildContext context, GoRouterState state) =>
factoryImpl(state).buildPageWithState(context, state);
- String? redirect(GoRouterState state) => factoryImpl(state).redirect();
+ FutureOr<String?> redirect(BuildContext context, GoRouterState state) =>
+ factoryImpl(state).redirect();
return GoRoute(
path: path,
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 486a717..b52cc22 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -29,13 +29,17 @@
///
/// The `redirect` does top-level redirection before the URIs are parsed by
/// the `routes`. Consider using [GoRoute.redirect] for individual route
-/// redirection.
+/// redirection. If [BuildContext.dependOnInheritedWidgetOfExactType] is used
+/// during the redirection (which is how `of` methods are usually implemented),
+/// a re-evaluation will be triggered when the [InheritedWidget] changes.
///
/// See also:
/// * [GoRoute], which provides APIs to define the routing table.
/// * [examples](https://github.com/flutter/packages/tree/main/packages/go_router/example),
/// which contains examples for different routing scenarios.
-class GoRouter extends ChangeNotifier with NavigatorObserver {
+class GoRouter extends ChangeNotifier
+ with NavigatorObserver
+ implements RouterConfig<RouteMatchList> {
/// Default constructor to configure a GoRouter with a routes builder
/// and an error page builder.
///
@@ -51,21 +55,11 @@
int redirectLimit = 5,
bool routerNeglect = false,
String? initialLocation,
- // TODO(johnpryan): Deprecate this parameter
- // See https://github.com/flutter/flutter/issues/108132
- UrlPathStrategy? urlPathStrategy,
List<NavigatorObserver>? observers,
bool debugLogDiagnostics = false,
- // TODO(johnpryan): Deprecate this parameter
- // See https://github.com/flutter/flutter/issues/108145
- GoRouterNavigatorBuilder? navigatorBuilder,
GlobalKey<NavigatorState>? navigatorKey,
String? restorationScopeId,
- }) {
- if (urlPathStrategy != null) {
- setUrlPathStrategy(urlPathStrategy);
- }
-
+ }) : backButtonDispatcher = RootBackButtonDispatcher() {
setLogging(enabled: debugLogDiagnostics);
WidgetsFlutterBinding.ensureInitialized();
@@ -73,7 +67,7 @@
_routeConfiguration = RouteConfiguration(
routes: routes,
- topRedirect: redirect ?? (_) => null,
+ topRedirect: redirect ?? (_, __) => null,
redirectLimit: redirectLimit,
navigatorKey: navigatorKey,
);
@@ -104,9 +98,10 @@
(BuildContext context, GoRouterState state, Navigator nav) =>
InheritedGoRouter(
goRouter: this,
- child: navigatorBuilder?.call(context, state, nav) ?? nav,
+ child: nav,
),
);
+
assert(() {
log.info('setting initial location $initialLocation');
return true;
@@ -118,15 +113,21 @@
late final GoRouterDelegate _routerDelegate;
late final GoRouteInformationProvider _routeInformationProvider;
+ @override
+ final BackButtonDispatcher backButtonDispatcher;
+
/// The router delegate. Provide this to the MaterialApp or CupertinoApp's
/// `.router()` constructor
+ @override
GoRouterDelegate get routerDelegate => _routerDelegate;
/// The route information provider used by [GoRouter].
+ @override
GoRouteInformationProvider get routeInformationProvider =>
_routeInformationProvider;
/// The route information parser used by [GoRouter].
+ @override
GoRouteInformationParser get routeInformationParser =>
_routeInformationParser;
@@ -185,8 +186,12 @@
return true;
}());
_routeInformationParser
- .parseRouteInformation(
- DebugGoRouteInformation(location: location, state: extra))
+ .parseRouteInformationWithDependencies(
+ DebugGoRouteInformation(location: location, state: extra),
+ // TODO(chunhtai): avoid accessing the context directly through global key.
+ // https://github.com/flutter/flutter/issues/99112
+ _routerDelegate.navigatorKey.currentContext!,
+ )
.then<void>((RouteMatchList matches) {
_routerDelegate.push(matches.last);
});
@@ -213,8 +218,11 @@
/// * [push] which pushes the location onto the page stack.
void replace(String location, {Object? extra}) {
routeInformationParser
- .parseRouteInformation(
+ .parseRouteInformationWithDependencies(
DebugGoRouteInformation(location: location, state: extra),
+ // TODO(chunhtai): avoid accessing the context directly through global key.
+ // https://github.com/flutter/flutter/issues/99112
+ _routerDelegate.navigatorKey.currentContext!,
)
.then<void>((RouteMatchList matchList) {
routerDelegate.replace(matchList.matches.last);
diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart
index ab540f6..f894488 100644
--- a/packages/go_router/lib/src/typedefs.dart
+++ b/packages/go_router/lib/src/typedefs.dart
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async' show FutureOr;
+
import 'package:flutter/widgets.dart';
import 'configuration.dart';
@@ -47,4 +49,5 @@
);
/// The signature of the redirect callback.
-typedef GoRouterRedirect = String? Function(GoRouterState state);
+typedef GoRouterRedirect = FutureOr<String?> Function(
+ BuildContext context, GoRouterState state);
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index fc097cf..5c67cb6 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,13 +1,13 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
-version: 4.5.1
+version: 5.0.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
environment:
sdk: ">=2.17.0 <3.0.0"
- flutter: ">=3.0.0"
+ flutter: ">=3.3.0"
dependencies:
collection: ^1.15.0
diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart
index 19830b0..92eb17f 100644
--- a/packages/go_router/test/builder_test.dart
+++ b/packages/go_router/test/builder_test.dart
@@ -22,7 +22,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
navigatorKey: GlobalKey<NavigatorState>(),
@@ -69,7 +69,7 @@
]),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
navigatorKey: GlobalKey<NavigatorState>(),
@@ -112,7 +112,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -167,7 +167,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -245,7 +245,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart
index b44d508..a01f7d1 100644
--- a/packages/go_router/test/configuration_test.dart
+++ b/packages/go_router/test/configuration_test.dart
@@ -50,7 +50,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -71,7 +71,7 @@
ShellRoute(routes: shellRouteChildren),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -111,7 +111,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -168,7 +168,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -217,7 +217,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
),
@@ -267,7 +267,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -325,7 +325,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
),
@@ -383,7 +383,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -410,7 +410,7 @@
]),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
@@ -439,7 +439,7 @@
),
],
redirectLimit: 10,
- topRedirect: (GoRouterState state) {
+ topRedirect: (BuildContext context, GoRouterState state) {
return null;
},
);
diff --git a/packages/go_router/test/custom_transition_page_test.dart b/packages/go_router/test/custom_transition_page_test.dart
index 5845be6..50fe625 100644
--- a/packages/go_router/test/custom_transition_page_test.dart
+++ b/packages/go_router/test/custom_transition_page_test.dart
@@ -24,9 +24,7 @@
);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index d227c3f..f60ce65 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -25,9 +25,8 @@
refreshListenable: refreshListenable,
);
await tester.pumpWidget(MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate));
+ routerConfig: router,
+ ));
return router;
}
@@ -127,9 +126,7 @@
);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
+ routerConfig: goRouter,
),
);
@@ -179,9 +176,7 @@
);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
+ routerConfig: goRouter,
),
);
diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart
index f2de79b..31361aa 100644
--- a/packages/go_router/test/go_route_test.dart
+++ b/packages/go_router/test/go_route_test.dart
@@ -15,6 +15,6 @@
});
test('does not throw when only redirect is provided', () {
- GoRoute(path: '/', redirect: (_) => '/a');
+ GoRoute(path: '/', redirect: (_, __) => '/a');
});
}
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 0c66068..67d3288 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -4,16 +4,11 @@
// ignore_for_file: cascade_invocations, diagnostic_describe_all_properties
-import 'dart:async';
-
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'package:go_router/src/delegate.dart';
+import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
-import 'package:go_router/src/misc/extensions.dart';
-import 'package:go_router/src/route.dart';
-import 'package:go_router/src/router.dart';
-import 'package:go_router/src/state.dart';
import 'package:logging/logging.dart';
import 'test_helpers.dart';
@@ -21,6 +16,17 @@
const bool enableLogs = true;
final Logger log = Logger('GoRouter tests');
+Future<void> sendPlatformUrl(String url) async {
+ final Map<String, dynamic> testRouteInformation = <String, dynamic>{
+ 'location': url,
+ };
+ final ByteData message = const JSONMethodCodec().encodeMethodCall(
+ MethodCall('pushRouteInformation', testRouteInformation),
+ );
+ await ServicesBinding.instance.defaultBinaryMessenger
+ .handlePlatformMessage('flutter/navigation', message, (_) {});
+}
+
void main() {
if (enableLogs) {
Logger.root.onRecord.listen((LogRecord e) => debugPrint('$e'));
@@ -191,7 +197,9 @@
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
- path: '/profile', builder: dummy, redirect: (_) => '/profile/foo'),
+ path: '/profile',
+ builder: dummy,
+ redirect: (_, __) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: dummy),
];
@@ -207,7 +215,9 @@
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
- path: '/profile', builder: dummy, redirect: (_) => '/profile/foo'),
+ path: '/profile',
+ builder: dummy,
+ redirect: (_, __) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: dummy),
];
@@ -833,7 +843,7 @@
GoRoute(
path: '/',
builder: dummy,
- redirect: (_) => '/family/f2',
+ redirect: (_, __) => '/family/f2',
),
GoRoute(
path: '/family/:fid',
@@ -932,12 +942,24 @@
],
),
];
+ bool redirected = false;
final GoRouter router = await createRouter(routes, tester,
- redirect: (GoRouterState state) =>
- state.subloc == '/login' ? null : '/login');
+ redirect: (BuildContext context, GoRouterState state) {
+ redirected = true;
+ return state.subloc == '/login' ? null : '/login';
+ });
expect(router.location, '/login');
+ expect(redirected, isTrue);
+
+ redirected = false;
+ // Directly set the url through platform message.
+ await sendPlatformUrl('/dummy');
+
+ await tester.pumpAndSettle();
+ expect(router.location, '/login');
+ expect(redirected, isTrue);
});
testWidgets('top-level redirect w/ named routes',
@@ -968,7 +990,7 @@
final GoRouter router = await createRouter(
routes,
tester,
- redirect: (GoRouterState state) =>
+ redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/login' ? null : state.namedLocation('login'),
);
expect(router.location, '/login');
@@ -985,7 +1007,7 @@
path: 'dummy',
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen(),
- redirect: (GoRouterState state) => '/login',
+ redirect: (BuildContext context, GoRouterState state) => '/login',
),
GoRoute(
path: 'login',
@@ -1002,6 +1024,49 @@
expect(router.location, '/login');
});
+ testWidgets('top-level redirect take priority over route level',
+ (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) =>
+ const HomeScreen(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'dummy',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen(),
+ redirect: (BuildContext context, GoRouterState state) {
+ // should never be reached.
+ assert(false);
+ return '/dummy2';
+ }),
+ GoRoute(
+ path: 'dummy2',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen()),
+ GoRoute(
+ path: 'login',
+ builder: (BuildContext context, GoRouterState state) =>
+ const LoginScreen()),
+ ],
+ ),
+ ];
+ bool redirected = false;
+ final GoRouter router = await createRouter(routes, tester,
+ redirect: (BuildContext context, GoRouterState state) {
+ redirected = true;
+ return state.subloc == '/login' ? null : '/login';
+ });
+ redirected = false;
+ // Directly set the url through platform message.
+ await sendPlatformUrl('/dummy');
+
+ await tester.pumpAndSettle();
+ expect(router.location, '/login');
+ expect(redirected, isTrue);
+ });
+
testWidgets('route-level redirect w/ named routes',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
@@ -1016,7 +1081,8 @@
path: 'dummy',
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen(),
- redirect: (GoRouterState state) => state.namedLocation('login'),
+ redirect: (BuildContext context, GoRouterState state) =>
+ state.namedLocation('login'),
),
GoRoute(
name: 'login',
@@ -1050,14 +1116,14 @@
path: 'dummy2',
builder: (BuildContext context, GoRouterState state) =>
const DummyScreen(),
- redirect: (GoRouterState state) => '/',
+ redirect: (BuildContext context, GoRouterState state) => '/',
),
],
),
];
final GoRouter router = await createRouter(routes, tester,
- redirect: (GoRouterState state) =>
+ redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/dummy1' ? '/dummy2' : null);
router.go('/dummy1');
await tester.pump();
@@ -1066,11 +1132,12 @@
testWidgets('top-level redirect loop', (WidgetTester tester) async {
final GoRouter router = await createRouter(<GoRoute>[], tester,
- redirect: (GoRouterState state) => state.subloc == '/'
- ? '/login'
- : state.subloc == '/login'
- ? '/'
- : null);
+ redirect: (BuildContext context, GoRouterState state) =>
+ state.subloc == '/'
+ ? '/login'
+ : state.subloc == '/login'
+ ? '/'
+ : null);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
@@ -1086,12 +1153,12 @@
GoRoute(
path: '/',
builder: dummy,
- redirect: (GoRouterState state) => '/login',
+ redirect: (BuildContext context, GoRouterState state) => '/login',
),
GoRoute(
path: '/login',
builder: dummy,
- redirect: (GoRouterState state) => '/',
+ redirect: (BuildContext context, GoRouterState state) => '/',
),
],
tester,
@@ -1111,11 +1178,11 @@
GoRoute(
path: '/login',
builder: dummy,
- redirect: (GoRouterState state) => '/',
+ redirect: (BuildContext context, GoRouterState state) => '/',
),
],
tester,
- redirect: (GoRouterState state) =>
+ redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/' ? '/login' : null,
);
@@ -1132,11 +1199,12 @@
final GoRouter router = await createRouter(
<GoRoute>[],
tester,
- redirect: (GoRouterState state) => state.subloc == '/'
- ? '/login?from=${state.location}'
- : state.subloc == '/login'
- ? '/'
- : null,
+ redirect: (BuildContext context, GoRouterState state) =>
+ state.subloc == '/'
+ ? '/login?from=${state.location}'
+ : state.subloc == '/login'
+ ? '/'
+ : null,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
@@ -1158,7 +1226,7 @@
GoRoute(
path: '/dummy',
builder: dummy,
- redirect: (GoRouterState state) => '/',
+ redirect: (BuildContext context, GoRouterState state) => '/',
),
];
@@ -1188,7 +1256,7 @@
routes,
tester,
initialLocation: '/login?from=/',
- redirect: (GoRouterState state) {
+ redirect: (BuildContext context, GoRouterState state) {
expect(Uri.parse(state.location).queryParameters, isNotEmpty);
expect(Uri.parse(state.subloc).queryParameters, isEmpty);
expect(state.path, isNull);
@@ -1210,7 +1278,7 @@
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/book/:bookId',
- redirect: (GoRouterState state) {
+ redirect: (BuildContext context, GoRouterState state) {
expect(state.location, loc);
expect(state.subloc, loc);
expect(state.path, '/book/:bookId');
@@ -1248,7 +1316,7 @@
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
- redirect: (GoRouterState s) {
+ redirect: (BuildContext context, GoRouterState s) {
expect(s.params['fid'], 'f2');
expect(s.params['pid'], 'p1');
return null;
@@ -1283,7 +1351,8 @@
final GoRouter router = await createRouter(
<GoRoute>[],
tester,
- redirect: (GoRouterState state) => '/${state.location}+',
+ redirect: (BuildContext context, GoRouterState state) =>
+ '/${state.location}+',
redirectLimit: 10,
);
@@ -1312,7 +1381,7 @@
builder: (BuildContext context, GoRouterState state) {
return const LoginScreen();
},
- redirect: (GoRouterState state) {
+ redirect: (BuildContext context, GoRouterState state) {
isCallRouteRedirect = true;
expect(state.extra, isNotNull);
return null;
@@ -1326,7 +1395,7 @@
final GoRouter router = await createRouter(
routes,
tester,
- redirect: (GoRouterState state) {
+ redirect: (BuildContext context, GoRouterState state) {
if (state.location == '/login') {
isCallTopRedirect = true;
expect(state.extra, isNotNull);
@@ -1342,6 +1411,52 @@
expect(isCallTopRedirect, true);
expect(isCallRouteRedirect, true);
});
+
+ testWidgets('parent route level redirect take priority over child',
+ (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) =>
+ const HomeScreen(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'dummy',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen(),
+ redirect: (BuildContext context, GoRouterState state) =>
+ '/other',
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'dummy2',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen(),
+ redirect: (BuildContext context, GoRouterState state) {
+ assert(false);
+ return '/other2';
+ },
+ ),
+ ]),
+ GoRoute(
+ path: 'other',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen()),
+ GoRoute(
+ path: 'other2',
+ builder: (BuildContext context, GoRouterState state) =>
+ const DummyScreen()),
+ ],
+ ),
+ ];
+
+ final GoRouter router = await createRouter(routes, tester);
+
+ // Directly set the url through platform message.
+ await sendPlatformUrl('/dummy/dummy2');
+
+ await tester.pumpAndSettle();
+ expect(router.location, '/other');
+ });
});
group('initial location', () {
@@ -1379,7 +1494,7 @@
GoRoute(
path: '/dummy',
builder: dummy,
- redirect: (GoRouterState state) => '/',
+ redirect: (BuildContext context, GoRouterState state) => '/',
),
];
@@ -1813,54 +1928,6 @@
);
});
- group('refresh listenable', () {
- late StreamController<int> streamController;
-
- setUpAll(() async {
- streamController = StreamController<int>.broadcast();
- await streamController.addStream(Stream<int>.value(0));
- });
-
- tearDownAll(() {
- streamController.close();
- });
-
- group('stream', () {
- test('no stream emits', () async {
- // Act
- final GoRouterRefreshStreamSpy notifyListener =
- GoRouterRefreshStreamSpy(
- streamController.stream,
- );
-
- // Assert
- expect(notifyListener.notifyCount, equals(1));
-
- // Cleanup
- notifyListener.dispose();
- });
-
- test('three stream emits', () async {
- // Arrange
- final List<int> toEmit = <int>[1, 2, 3];
-
- // Act
- final GoRouterRefreshStreamSpy notifyListener =
- GoRouterRefreshStreamSpy(
- streamController.stream,
- );
-
- await streamController.addStream(Stream<int>.fromIterable(toEmit));
-
- // Assert
- expect(notifyListener.notifyCount, equals(toEmit.length + 1));
-
- // Cleanup
- notifyListener.dispose();
- });
- });
- });
-
group('GoRouterHelper extensions', () {
final GlobalKey<DummyStatefulWidgetState> key =
GlobalKey<DummyStatefulWidgetState>();
@@ -1895,9 +1962,7 @@
GoRouterNamedLocationSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -1915,9 +1980,7 @@
final GoRouterGoSpy router = GoRouterGoSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -1934,9 +1997,7 @@
final GoRouterGoNamedSpy router = GoRouterGoNamedSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -1957,9 +2018,7 @@
final GoRouterPushSpy router = GoRouterPushSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -1976,9 +2035,7 @@
final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -1998,9 +2055,7 @@
final GoRouterPopSpy router = GoRouterPopSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
),
);
@@ -2462,30 +2517,5 @@
},
);
});
-
- testWidgets('uses navigatorBuilder when provided',
- (WidgetTester tester) async {
- final Func3<Widget, BuildContext, GoRouterState, Widget>
- navigatorBuilder = expectAsync3(fakeNavigationBuilder);
- final GoRouter router = GoRouter(
- initialLocation: '/',
- routes: <GoRoute>[
- GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
- GoRoute(
- path: '/error',
- builder: (_, __) => TestErrorScreen(TestFailure('exception')),
- ),
- ],
- navigatorBuilder: navigatorBuilder,
- );
-
- final GoRouterDelegate delegate = router.routerDelegate;
- delegate.builder.builderWithNav(
- DummyBuildContext(),
- GoRouterState(router.routeConfiguration,
- location: '/foo', subloc: '/bar', name: 'baz'),
- const Navigator(),
- );
- });
});
}
diff --git a/packages/go_router/test/helpers/error_screen_helpers.dart b/packages/go_router/test/helpers/error_screen_helpers.dart
index f804f44..26822e2 100644
--- a/packages/go_router/test/helpers/error_screen_helpers.dart
+++ b/packages/go_router/test/helpers/error_screen_helpers.dart
@@ -51,18 +51,14 @@
Widget materialAppRouterBuilder(GoRouter router) {
return MaterialApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
);
}
Widget cupertinoAppRouterBuilder(GoRouter router) {
return CupertinoApp.router(
- routeInformationProvider: router.routeInformationProvider,
- routeInformationParser: router.routeInformationParser,
- routerDelegate: router.routerDelegate,
+ routerConfig: router,
title: 'GoRouter Example',
);
}
diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart
index 920a61c..9fbb915 100644
--- a/packages/go_router/test/inherited_test.dart
+++ b/packages/go_router/test/inherited_test.dart
@@ -135,4 +135,7 @@
Object? extra}) {
latestPushedName = name;
}
+
+ @override
+ BackButtonDispatcher get backButtonDispatcher => RootBackButtonDispatcher();
}
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index aa67ad0..0165466 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -4,13 +4,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
import 'package:go_router/src/configuration.dart';
+import 'package:go_router/src/information_provider.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:go_router/src/parser.dart';
void main() {
- test('GoRouteInformationParser can parse route', () async {
+ Future<GoRouteInformationParser> createParser(
+ WidgetTester tester, {
+ required List<RouteBase> routes,
+ int redirectLimit = 5,
+ GoRouterRedirect? redirect,
+ }) async {
+ final GoRouter router = GoRouter(
+ routes: routes,
+ redirectLimit: redirectLimit,
+ redirect: redirect,
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routerConfig: router,
+ ));
+ return router.routeInformationParser;
+ }
+
+ testWidgets('GoRouteInformationParser can parse route',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -23,17 +43,18 @@
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
);
- RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+
+ RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/'), context);
List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 1);
expect(matches[0].queryParams.isEmpty, isTrue);
@@ -43,8 +64,9 @@
expect(matches[0].route, routes[0]);
final Object extra = Object();
- matchesObj = await parser.parseRouteInformation(
- RouteInformation(location: '/abc?def=ghi', state: extra));
+ matchesObj = await parser.parseRouteInformationWithDependencies(
+ DebugGoRouteInformation(location: '/abc?def=ghi', state: extra),
+ context);
matches = matchesObj.matches;
expect(matches.length, 2);
expect(matches[0].queryParams.length, 1);
@@ -90,7 +112,7 @@
final RouteConfiguration configuration = RouteConfiguration(
routes: routes,
redirectLimit: 100,
- topRedirect: (_) => null,
+ topRedirect: (_, __) => null,
navigatorKey: GlobalKey<NavigatorState>(),
);
@@ -133,7 +155,7 @@
final RouteConfiguration configuration = RouteConfiguration(
routes: routes,
redirectLimit: 100,
- topRedirect: (_) => null,
+ topRedirect: (_, __) => null,
navigatorKey: GlobalKey<NavigatorState>(),
);
@@ -147,7 +169,8 @@
);
});
- test('GoRouteInformationParser returns error when unknown route', () async {
+ testWidgets('GoRouteInformationParser returns error when unknown route',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -160,17 +183,18 @@
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
);
- final RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/def'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/def'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 1);
expect(matches[0].queryParams.isEmpty, isTrue);
@@ -181,7 +205,8 @@
'Exception: no routes for location: /def');
});
- test('GoRouteInformationParser can work with route parameters', () async {
+ testWidgets('GoRouteInformationParser can work with route parameters',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -194,17 +219,18 @@
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
);
- final RouteMatchList matchesObj = await parser.parseRouteInformation(
- const RouteInformation(location: '/123/family/456'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/123/family/456'),
+ context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 2);
@@ -222,9 +248,9 @@
expect(matches[1].encodedParams['fid'], '456');
});
- test(
+ testWidgets(
'GoRouteInformationParser processes top level redirect when there is no match',
- () async {
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -237,22 +263,22 @@
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (GoRouterState state) {
- if (state.location != '/123/family/345') {
- return '/123/family/345';
- }
- return null;
- },
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (BuildContext context, GoRouterState state) {
+ if (state.location != '/123/family/345') {
+ return '/123/family/345';
+ }
+ return null;
+ },
);
- final RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/random/uri'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/random/uri'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 2);
@@ -263,9 +289,9 @@
expect(matches[1].subloc, '/123/family/345');
});
- test(
+ testWidgets(
'GoRouteInformationParser can do route level redirect when there is a match',
- () async {
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -277,23 +303,23 @@
),
GoRoute(
path: 'redirect',
- redirect: (_) => '/123/family/345',
+ redirect: (_, __) => '/123/family/345',
builder: (_, __) => throw UnimplementedError(),
),
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
);
- final RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/redirect'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/redirect'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 2);
@@ -304,56 +330,57 @@
expect(matches[1].subloc, '/123/family/345');
});
- test('GoRouteInformationParser throws an exception when route is malformed',
- () async {
+ testWidgets(
+ 'GoRouteInformationParser throws an exception when route is malformed',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/abc',
builder: (_, __) => const Placeholder(),
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 100,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirectLimit: 100,
+ redirect: (_, __) => null,
);
+ final BuildContext context = tester.element(find.byType(Router<Object>));
expect(() async {
- await parser.parseRouteInformation(
- const RouteInformation(location: '::Not valid URI::'));
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '::Not valid URI::'),
+ context);
}, throwsA(isA<FormatException>()));
});
- test('GoRouteInformationParser returns an error if a redirect is detected.',
- () async {
+ testWidgets(
+ 'GoRouteInformationParser returns an error if a redirect is detected.',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/abc',
builder: (_, __) => const Placeholder(),
- redirect: (GoRouterState state) => state.location,
+ redirect: (BuildContext context, GoRouterState state) => state.location,
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 5,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirect: (_, __) => null,
);
- final RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/abd'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/abd'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches, hasLength(1));
expect(matches.first.error, isNotNull);
});
- test('Creates a match for ShellRoute', () async {
+ testWidgets('Creates a match for ShellRoute', (WidgetTester tester) async {
final List<RouteBase> routes = <RouteBase>[
ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
@@ -381,17 +408,16 @@
],
),
];
- final GoRouteInformationParser parser = GoRouteInformationParser(
- configuration: RouteConfiguration(
- routes: routes,
- redirectLimit: 5,
- topRedirect: (_) => null,
- navigatorKey: GlobalKey<NavigatorState>(),
- ),
+ final GoRouteInformationParser parser = await createParser(
+ tester,
+ routes: routes,
+ redirect: (_, __) => null,
);
- final RouteMatchList matchesObj = await parser
- .parseRouteInformation(const RouteInformation(location: '/a'));
+ final BuildContext context = tester.element(find.byType(Router<Object>));
+ final RouteMatchList matchesObj =
+ await parser.parseRouteInformationWithDependencies(
+ const DebugGoRouteInformation(location: '/a'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches, hasLength(2));
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index 71fb55ae..af9bf50 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -11,12 +11,8 @@
import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
-import 'package:go_router/src/typedefs.dart';
-Future<GoRouter> createGoRouter(
- WidgetTester tester, {
- GoRouterNavigatorBuilder? navigatorBuilder,
-}) async {
+Future<GoRouter> createGoRouter(WidgetTester tester) async {
final GoRouter goRouter = GoRouter(
initialLocation: '/',
routes: <GoRoute>[
@@ -26,12 +22,10 @@
builder: (_, __) => TestErrorScreen(TestFailure('Exception')),
),
],
- navigatorBuilder: navigatorBuilder,
);
await tester.pumpWidget(MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate));
+ routerConfig: goRouter,
+ ));
return goRouter;
}
@@ -143,20 +137,6 @@
}
}
-class GoRouterRefreshStreamSpy extends GoRouterRefreshStream {
- GoRouterRefreshStreamSpy(
- super.stream,
- ) : notifyCount = 0;
-
- late int notifyCount;
-
- @override
- void notifyListeners() {
- notifyCount++;
- super.notifyListeners();
- }
-}
-
Future<GoRouter> createRouter(
List<RouteBase> routes,
WidgetTester tester, {
@@ -176,9 +156,7 @@
);
await tester.pumpWidget(
MaterialApp.router(
- routeInformationProvider: goRouter.routeInformationProvider,
- routeInformationParser: goRouter.routeInformationParser,
- routerDelegate: goRouter.routerDelegate,
+ routerConfig: goRouter,
),
);
return goRouter;