Skip to content

Commit

Permalink
[go_router] Adds onException to GoRouter constructor. (#4216)
Browse files Browse the repository at this point in the history
  • Loading branch information
chunhtai committed Jun 23, 2023
1 parent 6d8782d commit 08425c5
Show file tree
Hide file tree
Showing 19 changed files with 420 additions and 99 deletions.
6 changes: 5 additions & 1 deletion packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 8.2.0

- Adds onException to GoRouter constructor.

## 8.1.0

- Adds parent navigator key to ShellRoute and StatefulShellRoute.
Expand All @@ -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

Expand Down
32 changes: 27 additions & 5 deletions packages/go_router/doc/error-handling.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions packages/go_router/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ An example to demonstrate how to use handle a sign-in flow with a stream authent
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`

Expand Down
87 changes: 87 additions & 0 deletions packages/go_router/example/lib/exception_handling.dart
Original file line number Diff line number Diff line change
@@ -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"),
),
);
}
}
22 changes: 6 additions & 16 deletions packages/go_router/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,9 @@ class HomeScreen extends StatelessWidget {
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'),
),
),
);
Expand All @@ -82,14 +77,9 @@ class DetailsScreen extends StatelessWidget {
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'),
),
),
);
Expand Down
23 changes: 9 additions & 14 deletions packages/go_router/example/lib/redirection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,15 @@ class LoginScreen extends StatelessWidget {
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');

// router will automatically redirect from /login to / using
// refreshListenable
},
child: const Text('Login'),
),
],
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'),
),
),
);
Expand Down
18 changes: 18 additions & 0 deletions packages/go_router/example/test/exception_handling_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
}
67 changes: 43 additions & 24 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,33 @@ class RouteConfiguration {
}

/// 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;

Expand Down Expand Up @@ -257,7 +274,8 @@ class RouteConfiguration {
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,
Expand Down Expand Up @@ -411,19 +429,7 @@ class RouteConfiguration {
// 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?) {
Expand Down Expand Up @@ -485,9 +491,9 @@ class RouteConfiguration {
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);
}
}

Expand All @@ -500,19 +506,32 @@ class RouteConfiguration {
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);

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
Expand Down
Loading

0 comments on commit 08425c5

Please sign in to comment.