[go_router] Adds onException to GoRouter constructor. (#4216)
fixes https://github.com/flutter/flutter/issues/108144
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index b530a9a..3dc0e2f 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 8.2.0
+
+- Adds onException to GoRouter constructor.
+
## 8.1.0
- Adds parent navigator key to ShellRoute and StatefulShellRoute.
@@ -8,7 +12,7 @@
## 8.0.4
-- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension.
+- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension.
## 8.0.3
diff --git a/packages/go_router/doc/error-handling.md b/packages/go_router/doc/error-handling.md
index 1021189..0dd1de8 100644
--- a/packages/go_router/doc/error-handling.md
+++ b/packages/go_router/doc/error-handling.md
@@ -1,12 +1,34 @@
-By default, go_router comes with default error screens for both `MaterialApp`
-and `CupertinoApp` as well as a default error screen in the case that none is
-used. You can also replace the default error screen by using the
-[errorBuilder](https://pub.dev/documentation/go_router/latest/go_router/GoRouter/GoRouter.html)
-parameter:
+There are several kinds of errors or exceptions in go_router.
+* GoError and AssertionError
+
+This kind of errors are thrown when go_router is used incorrectly, for example, if the root
+[GoRoute.path](https://pub.dev/documentation/go_router/latest/go_router/GoRoute/path.html) does
+not start with `/` or a builder in GoRoute is not provided. These errors should not be caught and
+must be fixed in code in order to use go_router.
+
+* GoException
+
+This kind of exception are thrown when the configuration of go_router cannot handle incoming requests
+from users or other part of the code. For example, an GoException is thrown when user enter url that
+can't be parsed according to pattern specified in the `GoRouter.routes`. These exceptions can be
+handled in various callbacks.
+
+Once can provide a callback to `GoRouter.onException` to handle this exception. In this callback,
+one can choose to ignore, redirect, or push different pages depending on the situation.
+See [Exception Handling](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/exception_handling.dart)
+on a runnable example.
+
+The `GoRouter.errorBuilder` and `GoRouter.errorPageBuilder` can also be used to handle exceptions.
```dart
GoRouter(
/* ... */
errorBuilder: (context, state) => ErrorScreen(state.error),
);
```
+
+By default, go_router comes with default error screens for both `MaterialApp`
+and `CupertinoApp` as well as a default error screen in the case that none is
+used.
+
+**Note** the `GoRouter.onException` supersedes other exception handling APIs.
\ No newline at end of file
diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md
index f4c1c0f..55f75bc 100644
--- a/packages/go_router/example/README.md
+++ b/packages/go_router/example/README.md
@@ -36,6 +36,11 @@
An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a
`BottomNavigationBar`.
+## [Exception Handling](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/exception_handling.dart)
+`flutter run lib/exception_handling.dart`
+
+An example to demonstrate how to handle exception in go_router.
+
## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books)
`flutter run lib/books/main.dart`
diff --git a/packages/go_router/example/lib/exception_handling.dart b/packages/go_router/example/lib/exception_handling.dart
new file mode 100644
index 0000000..583e35e
--- /dev/null
+++ b/packages/go_router/example/lib/exception_handling.dart
@@ -0,0 +1,87 @@
+// 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';
+
+/// This sample app shows how to use `GoRouter.onException` to redirect on
+/// exception.
+///
+/// The first route '/' is mapped to [HomeScreen], and the second route
+/// '/404' is mapped to [NotFoundScreen].
+///
+/// Any other unknown route or exception is redirected to `/404`.
+void main() => runApp(const MyApp());
+
+/// The route configuration.
+final GoRouter _router = GoRouter(
+ onException: (_, GoRouterState state, GoRouter router) {
+ router.go('/404', extra: state.location);
+ },
+ routes: <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) {
+ return const HomeScreen();
+ },
+ ),
+ GoRoute(
+ path: '/404',
+ builder: (BuildContext context, GoRouterState state) {
+ return NotFoundScreen(uri: state.extra as String? ?? '');
+ },
+ ),
+ ],
+);
+
+/// The main app.
+class MyApp extends StatelessWidget {
+ /// Constructs a [MyApp]
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp.router(
+ routerConfig: _router,
+ );
+ }
+}
+
+/// The home screen
+class HomeScreen extends StatelessWidget {
+ /// Constructs a [HomeScreen]
+ const HomeScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Home Screen')),
+ body: Center(
+ child: ElevatedButton(
+ onPressed: () => context.go('/some-unknown-route'),
+ child: const Text('Simulates user entering unknown url'),
+ ),
+ ),
+ );
+ }
+}
+
+/// The not found screen
+class NotFoundScreen extends StatelessWidget {
+ /// Constructs a [HomeScreen]
+ const NotFoundScreen({super.key, required this.uri});
+
+ /// The uri that can not be found.
+ final String uri;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Page Not Found')),
+ body: Center(
+ child: Text("Can't find a page for: $uri"),
+ ),
+ );
+ }
+}
diff --git a/packages/go_router/example/lib/main.dart b/packages/go_router/example/lib/main.dart
index e452471..d159648 100644
--- a/packages/go_router/example/lib/main.dart
+++ b/packages/go_router/example/lib/main.dart
@@ -58,14 +58,9 @@
return Scaffold(
appBar: AppBar(title: const Text('Home Screen')),
body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <Widget>[
- ElevatedButton(
- onPressed: () => context.go('/details'),
- child: const Text('Go to the Details screen'),
- ),
- ],
+ child: ElevatedButton(
+ onPressed: () => context.go('/details'),
+ child: const Text('Go to the Details screen'),
),
),
);
@@ -82,14 +77,9 @@
return Scaffold(
appBar: AppBar(title: const Text('Details Screen')),
body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: <ElevatedButton>[
- ElevatedButton(
- onPressed: () => context.go('/'),
- child: const Text('Go back to the Home screen'),
- ),
- ],
+ child: ElevatedButton(
+ onPressed: () => context.go('/'),
+ child: const Text('Go back to the Home screen'),
),
),
);
diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart
index 868944c..aded4b7 100644
--- a/packages/go_router/example/lib/redirection.dart
+++ b/packages/go_router/example/lib/redirection.dart
@@ -104,20 +104,15 @@
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: ElevatedButton(
+ onPressed: () {
+ // log a user in, letting all the listeners know
+ context.read<LoginInfo>().login('test-user');
- // router will automatically redirect from /login to / using
- // refreshListenable
- },
- child: const Text('Login'),
- ),
- ],
+ // router will automatically redirect from /login to / using
+ // refreshListenable
+ },
+ child: const Text('Login'),
),
),
);
diff --git a/packages/go_router/example/test/exception_handling_test.dart b/packages/go_router/example/test/exception_handling_test.dart
new file mode 100644
index 0000000..68a7310
--- /dev/null
+++ b/packages/go_router/example/test/exception_handling_test.dart
@@ -0,0 +1,18 @@
+// 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_test/flutter_test.dart';
+import 'package:go_router_examples/exception_handling.dart' as example;
+
+void main() {
+ testWidgets('example works', (WidgetTester tester) async {
+ await tester.pumpWidget(const example.MyApp());
+ expect(find.text('Simulates user entering unknown url'), findsOneWidget);
+
+ await tester.tap(find.text('Simulates user entering unknown url'));
+ await tester.pumpAndSettle();
+ expect(find.text("Can't find a page for: /some-unknown-route"),
+ findsOneWidget);
+ });
+}
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index 865007d..a1969d6 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -184,16 +184,33 @@
}
/// The match used when there is an error during parsing.
- static RouteMatchList _errorRouteMatchList(Uri uri, String errorMessage) {
- final Exception error = Exception(errorMessage);
+ static RouteMatchList _errorRouteMatchList(Uri uri, GoException exception) {
return RouteMatchList(
matches: const <RouteMatch>[],
- error: error,
+ error: exception,
uri: uri,
pathParameters: const <String, String>{},
);
}
+ /// Builds a [GoRouterState] suitable for top level callback such as
+ /// `GoRouter.redirect` or `GoRouter.onException`.
+ GoRouterState buildTopLevelGoRouterState(RouteMatchList matchList) {
+ return GoRouterState(
+ this,
+ location: matchList.uri.toString(),
+ // No name available at the top level trim the query params off the
+ // sub-location to match route.redirect
+ fullPath: matchList.fullPath,
+ pathParameters: matchList.pathParameters,
+ matchedLocation: matchList.uri.path,
+ queryParameters: matchList.uri.queryParameters,
+ queryParametersAll: matchList.uri.queryParametersAll,
+ extra: matchList.extra,
+ pageKey: const ValueKey<String>('topLevel'),
+ );
+ }
+
/// The list of top level routes used by [GoRouterDelegate].
final List<RouteBase> routes;
@@ -257,7 +274,8 @@
final List<RouteMatch>? matches = _getLocRouteMatches(uri, pathParameters);
if (matches == null) {
- return _errorRouteMatchList(uri, 'no routes for location: $uri');
+ return _errorRouteMatchList(
+ uri, GoException('no routes for location: $uri'));
}
return RouteMatchList(
matches: matches,
@@ -411,19 +429,7 @@
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = topRedirect(
context,
- GoRouterState(
- this,
- location: prevLocation,
- // No name available at the top level trim the query params off the
- // sub-location to match route.redirect
- fullPath: prevMatchList.fullPath,
- pathParameters: prevMatchList.pathParameters,
- matchedLocation: prevMatchList.uri.path,
- queryParameters: prevMatchList.uri.queryParameters,
- queryParametersAll: prevMatchList.uri.queryParametersAll,
- extra: prevMatchList.extra,
- pageKey: const ValueKey<String>('topLevel'),
- ),
+ buildTopLevelGoRouterState(prevMatchList),
);
if (topRedirectResult is String?) {
@@ -485,9 +491,9 @@
final RouteMatchList newMatch = findMatch(newLocation);
_addRedirect(redirectHistory, newMatch, previousLocation);
return newMatch;
- } on RedirectionError catch (e) {
- log.info('Redirection error: ${e.message}');
- return _errorRouteMatchList(e.location, e.message);
+ } on GoException catch (e) {
+ log.info('Redirection exception: ${e.message}');
+ return _errorRouteMatchList(previousLocation, e);
}
}
@@ -500,12 +506,18 @@
Uri prevLocation,
) {
if (redirects.contains(newMatch)) {
- throw RedirectionError('redirect loop detected',
- <RouteMatchList>[...redirects, newMatch], prevLocation);
+ throw GoException(
+ 'redirect loop detected ${_formatRedirectionHistory(<RouteMatchList>[
+ ...redirects,
+ newMatch
+ ])}');
}
if (redirects.length > redirectLimit) {
- throw RedirectionError('too many redirects',
- <RouteMatchList>[...redirects, newMatch], prevLocation);
+ throw GoException(
+ 'too many redirects ${_formatRedirectionHistory(<RouteMatchList>[
+ ...redirects,
+ newMatch
+ ])}');
}
redirects.add(newMatch);
@@ -513,6 +525,13 @@
log.info('redirecting to $newMatch');
}
+ String _formatRedirectionHistory(List<RouteMatchList> redirections) {
+ return redirections
+ .map<String>(
+ (RouteMatchList routeMatches) => routeMatches.uri.toString())
+ .join(' => ');
+ }
+
/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 3798671..1d382db 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -7,6 +7,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
+import '../go_router.dart';
import 'builder.dart';
import 'configuration.dart';
import 'match.dart';
@@ -25,6 +26,7 @@
required GoRouterWidgetBuilder? errorBuilder,
required List<NavigatorObserver> observers,
required this.routerNeglect,
+ required this.onException,
String? restorationScopeId,
}) : _configuration = configuration {
builder = RouteBuilder(
@@ -45,6 +47,13 @@
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
+ /// The exception handler that is called when parser can't handle the incoming
+ /// uri.
+ ///
+ /// If this is null, the exception is handled in the
+ /// [RouteBuilder.errorPageBuilder] or [RouteBuilder.errorBuilder].
+ final GoExceptionHandler? onException;
+
final RouteConfiguration _configuration;
_NavigatorStateIterator _createNavigatorStateIterator() =>
@@ -131,9 +140,11 @@
/// For use by the Router architecture as part of the RouterDelegate.
@override
Future<void> setNewRoutePath(RouteMatchList configuration) {
- currentConfiguration = configuration;
+ if (currentConfiguration != configuration) {
+ currentConfiguration = configuration;
+ notifyListeners();
+ }
assert(currentConfiguration.isNotEmpty || currentConfiguration.isError);
- notifyListeners();
// 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/match.dart b/packages/go_router/lib/src/match.dart
index e048331..865ab0b 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -10,6 +10,7 @@
import 'package:flutter/widgets.dart';
import 'configuration.dart';
+import 'misc/errors.dart';
import 'path_utils.dart';
/// An matched result by matching a [RouteBase] against a location.
@@ -183,7 +184,7 @@
final Object? extra;
/// An exception if there was an error during matching.
- final Exception? error;
+ final GoException? error;
/// the full path pattern that matches the uri.
///
diff --git a/packages/go_router/lib/src/misc/errors.dart b/packages/go_router/lib/src/misc/errors.dart
index 045e2ea..7f16378 100644
--- a/packages/go_router/lib/src/misc/errors.dart
+++ b/packages/go_router/lib/src/misc/errors.dart
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import '../match.dart';
-
/// Thrown when [GoRouter] is used incorrectly.
class GoError extends Error {
/// Constructs a [GoError]
@@ -16,23 +14,14 @@
String toString() => 'GoError: $message';
}
-/// A configuration error detected while processing redirects.
-class RedirectionError extends Error implements UnsupportedError {
- /// RedirectionError constructor.
- RedirectionError(this.message, this.matches, this.location);
+/// Thrown when [GoRouter] can not handle a user request.
+class GoException implements Exception {
+ /// Creates an exception with message describing the reason.
+ GoException(this.message);
- /// The matches that were found while processing redirects.
- final List<RouteMatchList> matches;
-
- @override
+ /// The reason that causes this exception.
final String message;
- /// The location that was originally navigated to, before redirection began.
- final Uri location;
-
@override
- String toString() => '${super.toString()} ${<String>[
- ...matches
- .map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
- ].join(' => ')}';
+ String toString() => 'GoException: $message';
}
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 1790241..1a9dcca 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -14,17 +14,36 @@
import 'logging.dart';
import 'match.dart';
+/// The function signature of [GoRouteInformationParser.onParserException].
+///
+/// The `routeMatchList` parameter contains the exception explains the issue
+/// occurred.
+///
+/// The returned [RouteMatchList] is used as parsed result for the
+/// [GoRouterDelegate].
+typedef ParserExceptionHandler = RouteMatchList Function(
+ BuildContext context,
+ RouteMatchList routeMatchList,
+);
+
/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher].
/// Also performs redirection using [RouteRedirector].
class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
/// Creates a [GoRouteInformationParser].
GoRouteInformationParser({
required this.configuration,
+ required this.onParserException,
}) : _routeMatchListCodec = RouteMatchListCodec(configuration);
/// The route configuration used for parsing [RouteInformation]s.
final RouteConfiguration configuration;
+ /// The exception handler that is called when parser can't handle the incoming
+ /// uri.
+ ///
+ /// This method must return a [RouteMatchList] for the parsed result.
+ final ParserExceptionHandler? onParserException;
+
final RouteMatchListCodec _routeMatchListCodec;
final Random _random = Random();
@@ -50,7 +69,13 @@
// the state.
final RouteMatchList matchList =
_routeMatchListCodec.decode(state as Map<Object?, Object?>);
- return debugParserFuture = _redirect(context, matchList);
+ return debugParserFuture = _redirect(context, matchList)
+ .then<RouteMatchList>((RouteMatchList value) {
+ if (value.isError && onParserException != null) {
+ return onParserException!(context, value);
+ }
+ return value;
+ });
}
late final RouteMatchList initialMatches;
@@ -70,6 +95,9 @@
context,
initialMatches,
).then<RouteMatchList>((RouteMatchList matchList) {
+ if (matchList.isError && onParserException != null) {
+ return onParserException!(context, matchList);
+ }
return _updateRouteMatchList(
matchList,
baseRouteMatchList: state.baseRouteMatchList,
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 15ceea6..745fc3a 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -14,6 +14,15 @@
import 'parser.dart';
import 'typedefs.dart';
+/// The function signature of [GoRouter.onException].
+///
+/// Use `state.error` to access the exception.
+typedef GoExceptionHandler = void Function(
+ BuildContext context,
+ GoRouterState state,
+ GoRouter router,
+);
+
/// The route configuration for the app.
///
/// The `routes` list specifies the top-level routes for the app. It must not be
@@ -30,6 +39,15 @@
/// implemented), a re-evaluation will be triggered when the [InheritedWidget]
/// changes.
///
+/// To handle exceptions, use one of `onException`, `errorBuilder`, or
+/// `errorPageBuilder`. The `onException` is called when an exception is thrown.
+/// If `onException` is not provided, the exception is passed to
+/// `errorPageBuilder` to build a page for the Router if it is not null;
+/// otherwise, it is passed to `errorBuilder` instead. If none of them are
+/// provided, go_router builds a default error screen to show the exception.
+/// See [Error handling](https://pub.dev/documentation/go_router/latest/topics/error-handling.html)
+/// for more details.
+///
/// See also:
/// * [Configuration](https://pub.dev/documentation/go_router/latest/topics/Configuration-topic.html)
/// * [GoRoute], which provides APIs to define the routing table.
@@ -51,8 +69,7 @@
/// The `routes` must not be null and must contain an [GoRouter] to match `/`.
GoRouter({
required List<RouteBase> routes,
- // TODO(johnpryan): Change to a route, improve error API
- // See https://github.com/flutter/flutter/issues/108144
+ GoExceptionHandler? onException,
GoRouterPageBuilder? errorPageBuilder,
GoRouterWidgetBuilder? errorBuilder,
GoRouterRedirect? redirect,
@@ -70,6 +87,12 @@
initialExtra == null || initialLocation != null,
'initialLocation must be set in order to use initialExtra',
),
+ assert(
+ (onException == null ? 0 : 1) +
+ (errorPageBuilder == null ? 0 : 1) +
+ (errorBuilder == null ? 0 : 1) <
+ 2,
+ 'Only one of onException, errorPageBuilder, or errorBuilder can be provided.'),
assert(_debugCheckPath(routes, true)),
assert(
_debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
@@ -90,7 +113,21 @@
navigatorKey: navigatorKey,
);
+ final ParserExceptionHandler? parserExceptionHandler;
+ if (onException != null) {
+ parserExceptionHandler =
+ (BuildContext context, RouteMatchList routeMatchList) {
+ onException(context,
+ configuration.buildTopLevelGoRouterState(routeMatchList), this);
+ // Avoid updating GoRouterDelegate if onException is provided.
+ return routerDelegate.currentConfiguration;
+ };
+ } else {
+ parserExceptionHandler = null;
+ }
+
routeInformationParser = GoRouteInformationParser(
+ onParserException: parserExceptionHandler,
configuration: configuration,
);
@@ -102,6 +139,7 @@
routerDelegate = GoRouterDelegate(
configuration: configuration,
+ onException: onException,
errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder,
routerNeglect: routerNeglect,
diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart
index 13ee9ad..89400e3 100644
--- a/packages/go_router/lib/src/state.dart
+++ b/packages/go_router/lib/src/state.dart
@@ -72,8 +72,8 @@
/// An extra object to pass along with the navigation.
final Object? extra;
- /// The error associated with this match.
- final Exception? error;
+ /// The error associated with this sub-route.
+ final GoException? error;
/// A unique string key for this sub-route.
/// E.g.
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index ded370a..7737087 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
-version: 8.1.0
+version: 8.2.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
diff --git a/packages/go_router/test/exception_handling_test.dart b/packages/go_router/test/exception_handling_test.dart
new file mode 100644
index 0000000..5afd103
--- /dev/null
+++ b/packages/go_router/test/exception_handling_test.dart
@@ -0,0 +1,97 @@
+// 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:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+
+import 'test_helpers.dart';
+
+void main() {
+ testWidgets('throws if more than one exception handlers are provided.',
+ (WidgetTester tester) async {
+ bool thrown = false;
+ try {
+ GoRouter(
+ routes: <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (_, GoRouterState state) => const Text('home')),
+ ],
+ errorBuilder: (_, __) => const Text(''),
+ onException: (_, __, ___) {},
+ );
+ } on Error {
+ thrown = true;
+ }
+ expect(thrown, true);
+
+ thrown = false;
+ try {
+ GoRouter(
+ routes: <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (_, GoRouterState state) => const Text('home')),
+ ],
+ errorBuilder: (_, __) => const Text(''),
+ errorPageBuilder: (_, __) => const MaterialPage<void>(child: Text('')),
+ );
+ } on Error {
+ thrown = true;
+ }
+ expect(thrown, true);
+
+ thrown = false;
+ try {
+ GoRouter(
+ routes: <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (_, GoRouterState state) => const Text('home')),
+ ],
+ onException: (_, __, ___) {},
+ errorPageBuilder: (_, __) => const MaterialPage<void>(child: Text('')),
+ );
+ } on Error {
+ thrown = true;
+ }
+ expect(thrown, true);
+ });
+
+ group('onException', () {
+ testWidgets('can redirect.', (WidgetTester tester) async {
+ final GoRouter router = await createRouter(<RouteBase>[
+ GoRoute(
+ path: '/error',
+ builder: (_, GoRouterState state) =>
+ Text('redirected ${state.extra}')),
+ ], tester,
+ onException: (_, GoRouterState state, GoRouter router) =>
+ router.go('/error', extra: state.location));
+ expect(find.text('redirected /'), findsOneWidget);
+
+ router.go('/some-other-location');
+ await tester.pumpAndSettle();
+ expect(find.text('redirected /some-other-location'), findsOneWidget);
+ });
+
+ testWidgets('stays on the same page if noop.', (WidgetTester tester) async {
+ final GoRouter router = await createRouter(
+ <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (_, GoRouterState state) => const Text('home')),
+ ],
+ tester,
+ onException: (_, __, ___) {},
+ );
+ expect(find.text('home'), findsOneWidget);
+
+ router.go('/some-other-location');
+ await tester.pumpAndSettle();
+ expect(find.text('home'), findsOneWidget);
+ });
+ });
+}
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 2ebb4af..1d4b059 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -127,7 +127,12 @@
GoRoute(path: '/', builder: dummy),
];
- final GoRouter router = await createRouter(routes, tester);
+ final GoRouter router = await createRouter(
+ routes,
+ tester,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
+ );
router.go('/foo');
await tester.pumpAndSettle();
final List<RouteMatch> matches =
@@ -1889,14 +1894,18 @@
});
testWidgets('top-level redirect loop', (WidgetTester tester) async {
- final GoRouter router = await createRouter(<GoRoute>[], tester,
- redirect: (BuildContext context, GoRouterState state) =>
- state.matchedLocation == '/'
- ? '/login'
- : state.matchedLocation == '/login'
- ? '/'
- : null);
-
+ final GoRouter router = await createRouter(
+ <GoRoute>[],
+ tester,
+ redirect: (BuildContext context, GoRouterState state) =>
+ state.matchedLocation == '/'
+ ? '/login'
+ : state.matchedLocation == '/login'
+ ? '/'
+ : null,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
+ );
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
@@ -1921,6 +1930,8 @@
),
],
tester,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
@@ -1944,6 +1955,8 @@
tester,
redirect: (BuildContext context, GoRouterState state) =>
state.matchedLocation == '/' ? '/login' : null,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
@@ -1966,6 +1979,8 @@
: state.matchedLocation == '/login'
? '/'
: null,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
);
final List<RouteMatch> matches =
@@ -2153,6 +2168,8 @@
tester,
redirect: (BuildContext context, GoRouterState state) =>
'/${state.location}+',
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ TestErrorScreen(state.error!),
redirectLimit: 10,
);
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index 253790f..ad2573f 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -198,7 +198,7 @@
expect(matchesObj.uri.toString(), '/def');
expect(matchesObj.extra, isNull);
expect(matchesObj.error!.toString(),
- 'Exception: no routes for location: /def');
+ 'GoException: no routes for location: /def');
});
testWidgets(
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index 5383838..229452c 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -149,16 +149,16 @@
GlobalKey<NavigatorState>? navigatorKey,
GoRouterWidgetBuilder? errorBuilder,
String? restorationScopeId,
+ GoExceptionHandler? onException,
}) async {
final GoRouter goRouter = GoRouter(
routes: routes,
redirect: redirect,
initialLocation: initialLocation,
+ onException: onException,
initialExtra: initialExtra,
redirectLimit: redirectLimit,
- errorBuilder: errorBuilder ??
- (BuildContext context, GoRouterState state) =>
- TestErrorScreen(state.error!),
+ errorBuilder: errorBuilder,
navigatorKey: navigatorKey,
restorationScopeId: restorationScopeId,
);