5.2 KiB
Navigation and Routing
A sample that shows how to use the Router API to handle common navigation scenarios.
Goals
- Demonstrate common navigation scenarios:
- Parsing path parameters ('/user/:id')
- Sign in (validation / guards)
- Nested navigation
- Provide a reusable implementation of RouterDelegate and RouteInformationParser
- Demonstrate how deep linking is configured on iOS and Android
- Demonstrate how to use the Link widget from
package:url_Launcher
with the Router API.
How it works
The top-level widget, Bookstore
, sets up the state for this app. It places
three InheritedNotifier
widgets in the tree: RouteStateScope
,
BookstoreAuthScope
, and LibraryScope
, which provide the state for the
application:
RouteState
: stores the current route path (/book/1
) as aParsedRoute
object (see below).BookstoreAuthScope
: stores a mock authentication API,BookstoreAuth
.LibraryScope
: stores the data for the app,Library
.
The Bookstore
widget also uses the MaterialApp.router()
constructor to opt-in to the Router API. This constructor requires a
RouterDelegate and RouteInformationParser. This app uses the
routing.dart
library, described below.
routing.dart
This library contains a general-purpose routing solution for medium-sized apps. It implements these classes:
SimpleRouterDelegate
: ImplementsRouterDelegate
. UpdatesRouteState
when a new route has been pushed to the application by the operating system. Also notifies theRouter
widget whenever theRouteState
changes.TemplateRouteParser
: Implements RouteInformationParser. Parses the incoming route path into aParsedRoute
object. ARouteGuard
can be provided to guard access to certain routes.ParsedRoute
: Contains the current route location ("/user/2"), path parameters ({id: 2}), query parameters ("?search=abc"), and path template ("/user/:id")RouteState
: Stores the currentParsedRoute
.RouteGuard
: Guards access to routes. Can be overridden to redirect the incoming route if a condition isn't met.
App Structure
The SimpleRouterDelegate
constructor requires a WidgetBuilder
parameter and
a navigatorKey
. This app uses a BookstoreNavigator
widget, which configures
a Navigator
with a list of pages, based on the current RouteState
.
SimpleRouterDelegate(
routeState: routeState,
navigatorKey: navigatorKey,
builder: (context) => BookstoreNavigator(
navigatorKey: navigatorKey,
),
);
This Navigator
is configured to display either the sign-in screen or the
BookstoreScaffold
. An additional screen is stacked on top of the
BookstoreScaffold
if a book or author is currently selected:
return Navigator(
key: widget.navigatorKey,
onPopPage: (route, dynamic result) {
// ...
},
pages: [
if (routeState.route.pathTemplate == '/signin')
FadeTransitionPage<void>(
key: signInKey,
child: SignInScreen(),
),
else ...[
FadeTransitionPage<void>(
key: scaffoldKey,
child: BookstoreScaffold(),
),
if (selectedBook != null)
MaterialPage<void>(
key: bookDetailsKey,
child: BookDetailsScreen(
book: selectedBook,
),
)
else if (selectedAuthor != null)
MaterialPage<void>(
key: authorDetailsKey,
child: AuthorDetailsScreen(
author: selectedAuthor,
),
),
],
],
);
The BookstoreScaffold
widget uses package:adaptive_navigation
to build a
navigation rail or bottom navigation bar based on the size of the screen. The
body of this screen is BookstoreScaffoldBody
, which configures a nested
Navigator to display either the AuthorsScreen
, SettingsScreen
, or
BooksScreen
widget.
Linking vs updating RouteState
There are two ways to change the current route, either by updating RouteState
,
which the RouterDelegate listens to, or use the Link widget from
package:url_launcher
. The SettingsScreen
widget demonstrates both options:
Link(
uri: Uri.parse('/book/0'),
builder: (context, followLink) {
return TextButton(
child: const Text('Go directly to /book/0 (Link)'),
onPressed: followLink,
);
},
),
TextButton(
child: const Text('Go directly to /book/0 (RouteState)'),
onPressed: () {
RouteStateScope.of(context)!.go('/book/0');
},
),
Questions/issues
If you have a general question about the Router API, the best places to go are:
If you run into an issue with the sample itself, please file an issue in the main Flutter repo.