go_router 0.5.2 go_router: ^0.5.2 copied to clipboard
A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more
go_router #
The goal of the go_router package is to
simplify use of the Router
in
Flutter as specified
by the MaterialApp.router
constructor.
By default, it requires an implementation of the
RouterDelegate
and
RouteInformationParser
classes. These two implementations themselves imply the definition of a custom
type to hold the app state that drives the creation of the
Navigator
. You
can read an excellent blog post on these requirements on
Medium.
This separation of responsibilities allows the Flutter developer to implement a
number of routing and navigation policies at the cost of
complexity.
The go_router makes three simplifying assumptions to reduce complexity:
- all routing in the app will happen via schemeless absolute URI-compliant names
- an entire stack of pages can be constructed from the route name alone
- the concept of "back" in your app is "up" the stack of pages
These assumptions allow go_router to provide a simpler implementation of your app's custom router regardless of the platform you're targeting. Specifically, since web users can enter arbitrary locations to navigate your app, go_router is designed to be able to handle arbitrary locations while still allowing an easy-to-use developer experience.
Getting Started #
To use the go_router package, follow these instructions.
Declarative Routing #
The go_router is governed by a set of routes which you specify via a routes builder function:
class App extends StatelessWidget {
...
late final _router = GoRouter(routes: _routesBuilder, error: _errorBuilder);
List<GoRoute> _routesBuilder(BuildContext context, String location) => [
GoRoute(
pattern: '/',
builder: (context, state) => MaterialPage<FamiliesPage>(
key: const ValueKey('FamiliesPage'),
child: FamiliesPage(families: Families.data),
),
),
GoRoute(
pattern: '/family/:fid',
builder: (context, state) {
final family = Families.family(state.args['fid']!);
return MaterialPage<FamilyPage>(
key: ValueKey(family),
child: FamilyPage(family: family),
);
},
),
GoRoute(
pattern: '/family/:fid/person/:pid',
builder: (context, state) {
final family = Families.family(state.args['fid']!);
final person = family.person(state.args['pid']!);
return MaterialPage<PersonPage>(
key: ValueKey(person),
child: PersonPage(family: family, person: person),
);
},
),
];
...
}
In this case, we've defined 3 routes. The route name patterns are defined and
implemented in the path_to_regexp
package, which gives you the ability to include regular expressions, e.g.
/family/:fid(f\d+)
. These route name patterns will be matched in order and
every pattern that matches a prefix of the location will be a page on the
navigation stack like so:
pattern | example location | navigation stack |
---|---|---|
/ |
/ |
FamiliesPage() ← |
/family/:fid |
/family/f1 |
FamiliesPage() FamilyPage(f1) ← |
/family/:fid/person/:pid |
/family/f1/person/p2 |
FamiliesPage() FamilyPage(f1) PersonPage(p2) ← |
The order of the patterns in the list of routes dictates the order in the
navigation stack. The navigation stack is used to pop up to the previous page in
the stack when the user press the Back button or your app calls
Navigation.pop()
.
In addition to the pattern, a GoRoute
contains a page builder function which
is called to create the page when a pattern is matched. That function can use
the arguments parsed from the pattern to do things like look up data to use to
initialize each page.
In addition, the go_router needs an error handler in case no page is found or if any of the page builder functions throws an exception, e.g.
class App extends StatelessWidget {
...
late final _router = GoRouter(routes: _routesBuilder, error: _errorBuilder);
...
Page<dynamic> _errorBuilder(BuildContext context, GoRouterState state) =>
MaterialPage<Four04Page>(
key: const ValueKey('Four04Page'),
child: Four04Page(message: state.error.toString()),
);
}
The GoRouterState
object contains the location that caused the exception and
the Exception
that was thrown attempting to navigate to that route.
With these two functions in hand, you can establish your app's custom routing
policy using the MaterialApp.router
constructor:
class App extends StatelessWidget {
App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
);
late final _router = GoRouter(routes: _routesBuilder, error: _errorBuilder);
List<GoRoute> _routesBuilder(BuildContext context, String location) => ...
Page<dynamic> _errorBuilder(BuildContext context, GoRouterState state) => ...
}
With the router in place, your app can now navigate between pages.
Navigation #
To navigate between pages, use the GoRouter.go
method:
// navigate using the GoRouter
onTap: () => GoRouter.of(context).go('/family/f1/person/p2')
The go_router also provides a simplified version using Dart extension methods:
// more easily navigate using the GoRouter
onTap: () => context.go('/family/f1/person/p2')
The simplified version maps directly to the more fully-specified version, so you
can use either. If you're curious, the ability to just call context.go(...)
and have the right thing happen is where the name of the go_router came from.
URL Path Strategy #
By default, Flutter adds a hash (#) into the URL for web apps:
The process for turning off the hash is
documented
but fiddly. The go_router has built-in support for setting the URL path
strategy, however, so you can simply call GoRouter.setUrlPathStrategy
before
calling runApp
and make your choice:
void main() {
// turn on the # in the URLs on the web (default)
// GoRouter.setUrlPathStrategy(UrlPathStrategy.hash);
// turn off the # in the URLs on the web
GoRouter.setUrlPathStrategy(UrlPathStrategy.path);
runApp(App());
}
Setting the path instead of the hash strategy turns off the # in the URLs:
While the docs imply -- and the code itself states -- that you need to set the
URL path strategy option before calling runApp
, the current implementation
seems to support setting this option afterwards, which means that if you're
creating your GoRouter
as part of your top widget's initialization, you should
be safe to use the urlPathStrategy
parameter instead of calling anything
before runApp
:
void main() => runApp(App());
/// sample app using the path URL strategy, i.e. no # in the URL path
class App extends StatelessWidget {
late final _router = GoRouter(
routes: _routesBuilder,
error: _errorBuilder,
// turn off the # in the URLs on the web
urlPathStrategy: UrlPathStrategy.path,
);
...
}
Finally, when you deploy your Flutter web app to a web server, it needs to be
configured such that every URL ends up at your Flutter web app's index.html
,
otherwise Flutter won't be able to route to your pages. If you're using Firebase
hosting, you can configure
rewrites to
cause all URLs to be rewritten to index.html
.
If you'd like to test your release build locally before publishing, and get that
cool redirect to index.html
feature, you can use flutter run
itself:
$ flutter run -d chrome --release lib/url_strategy.dart
Note that you have to run this command from a place where flutter run
can find
the web/index.html
file.
Of course, any local web server that can be configured to redirect all traffic
to index.html
will do, e.g.
live-server.
Deep Linking #
Flutter defines "deep linking" as "opening a URL displays that screen in your
app." Anything that's listed as a GoRoute
can be accessed via deep linking
across Android, iOS and the web. Support works out of the box for the web, of
course, via the address bar, but requires additional configuration for Android
and iOS as described in the Flutter
docs.
Conditional Routes #
The routes builder is called each time that the location changes, which allows
you to change the routes based on the location. Furthermore, if you'd like to
change the set of routes based on conditional app state, you can do so using
InheritedWidget
or one of it's wrappers. For example, imagine a simple class
to track the app's current logged in state:
class LoginInfo extends ChangeNotifier {
var _userName = '';
bool get loggedIn => _userName.isNotEmpty;
void login(String userName) {
_userName = userName;
notifyListeners();
}
}
Because the LoginInfo
is a ChangeNotifier
, it can accept listeners and
notify them of data changes. We can then use the provider
package (which is based on
InheritedWidget
) to drop an instance of LoginInfo
into the widget tree:
class App extends StatelessWidget {
// add the login info into the tree as app state that can change over time
@override
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>(
create: (context) => LoginInfo(),
child: MaterialApp.router(
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: 'Conditional Routes GoRouter Example',
),
);
...
}
Now imagine a login page that pulls the login info out of the widget tree and changes the login state as appropriate:
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(_title(context))),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
// log a user in, letting all the listeners know
onPressed: () => context.read<LoginInfo>().login('user1'),
child: const Text('Login'),
),
],
),
),
);
...
}
Notice the use of context.read
from the provider package to walk the widget
tree to find the login info and login a sample user. This causes the listeners
to this data to be notified and for any widgets listening for this change to
rebuild. We can then use this data when implementing the GoRouter
routes
builder to decide which routes are allowed:
class App extends StatelessWidget {
...
late final _router = GoRouter(routes: _routeBuilder, error: _errorBuilder);
// the routes when the user is logged in
final _loggedInRoutes = [
GoRoute(
pattern: '/',
builder: (context, state) => MaterialPage<FamiliesPage>(...),
),
GoRoute(
pattern: '/family/:fid',
builder: (context, state) => MaterialPage<FamilyPage>(...),
),
GoRoute(
pattern: '/family/:fid/person/:pid',
builder: (context, state) => MaterialPage<PersonPage>(...),
),
];
// the routes when the user is not logged in
final _loggedOutRoutes = [
GoRoute(
pattern: '/',
builder: (context, state) => MaterialPage<LoginPage>(...),
),
];
// changes in the login info will rebuild the stack of routes
List<GoRoute> _routeBuilder(BuildContext context, String location) =>
context.watch<LoginInfo>().loggedIn ? _loggedInRoutes : _loggedOutRoutes;
...
}
Here we've defined two lists of routes, one for when the user is logged in and
one for when they're not. Then, we use context.watch
to read the login info to
determine which list of routes to return. And because we used context.watch
instead of context.read
, whenever the login info object changes, the routes
builder is automatically called for the correct list of routes based on the
current app state.
Redirection #
Sometimes you want to redirect one route to another one, e.g. if the user is not
logged in. You can do that by passing a redirect function to the GoRouter
object, e.g.
late final _router = GoRouter(
routes: _routesBuilder,
error: _errorBuilder,
redirect: _redirect,
);
...
// redirect based on app and routing state
String? _redirect(BuildContext context, GoRouterState state) {
// watching LoginInfo will cause a change in LoginInfo to trigger routing
final loggedIn = context.watch<LoginInfo>().loggedIn;
final goingToLogin = state.pattern == '/login';
// the user is not logged in and not headed to /login, they need to login
if (!loggedIn && !goingToLogin) return '/login';
// the user is logged in and headed to /login, no need to login again
if (loggedIn && goingToLogin) return '/';
// no need to redirect at all
return null;
}
In this code, if the user is not logged in and not going to the /login
pattern, we redirect to /login
. Likewise, if the user is logged in but going
/login
, we redirect to /
. And because we're using context.watch
, when the
login state changes, the routes builder will be called again to generate and
match the routes.
Query Parameters #
If you'd like to use query parameters for navigation, you can; they will be
considered as optional for the purpose of matching a route but passed along as
arguments to the page builders. For example, if you'd like to redirect to
/login
with the original location so that after a successful login, the user
can be routed back to the original location, you can do that using query
paramaters:
late final _router = GoRouter(
routes: _routesBuilder,
error: _errorBuilder,
redirect: _redirect,
);
...
List<GoRoute> _routeBuilder(BuildContext context, String location) => [
...
GoRoute(
pattern: '/login',
builder: (context, state) => MaterialPage<LoginPage>(
key: const ValueKey('LoginPage'),
child: LoginPage(from: state.args['from']),
),
),
];
// redirect based on app and routing state
String? _redirect(BuildContext context, GoRouterState state) {
// watching LoginInfo will cause a change in LoginInfo to trigger routing
final loggedIn = context.watch<LoginInfo>().loggedIn;
final goingToLogin = state.pattern == '/login';
// the user is not logged in and not headed to /login, they need to login
if (!loggedIn && !goingToLogin) return '/login?from=${state.location}';
// the user is logged in and headed to /login, no need to login again
if (loggedIn && goingToLogin) return '/';
// no need to redirect at all
return null;
}
In this example, if the user isn't logged in, they're redirected to /login
with a from
query parameter set to the original location. When the /login
route is matched, the optional from
parameter is passed to the LoginPage
. In
the LoginPage
if the from
parameter was passed, we use it to go to the
original location after a successful login:
class LoginPage extends StatelessWidget {
final String? from;
const LoginPage({this.from, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text(_title(context))),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
// log a user in, letting all the listeners know
onPressed: () {
context.read<LoginInfo>().login('user1');
if (from != null) context.go(from!);
},
child: const Text('Login'),
),
],
),
),
);
}
A query parameter will not override a positional parameter or another query parameter set earlier in the location string.
Custom Builder #
As described, the go_router uses the list of GoRoute
objects to implement it's
routing policy using patterns to match and using the order of matches to create
the Navigator.pop()
implementation, etc. If you'd like to implement the
routing policy yourself, you can implement a widget builder that is given a
location and is responsible for producing a Navigator
. For example, this
builder can be implemented and passed to the GoRouter.builder
constructor like
so:
class App extends StatelessWidget {
...
late final _router = GoRouter.builder(builder: _builder);
Widget _builder(BuildContext context, String location) {
final locPages = <String, Page<dynamic>>{};
try {
final segments = Uri.parse(location).pathSegments;
// home page, i.e. '/'
{
const loc = '/';
final page = MaterialPage<FamiliesPage>(
key: const ValueKey('FamiliesPage'),
child: FamiliesPage(families: Families.data),
);
locPages[loc] = page;
}
// family page, e.g. '/family/:fid
if (segments.length >= 2 && segments[0] == 'family') {
final fid = segments[1];
final family = Families.family(fid);
final loc = '/family/$fid';
final page = MaterialPage<FamilyPage>(
key: ValueKey(family),
child: FamilyPage(family: family),
);
locPages[loc] = page;
}
// person page, e.g. '/family/:fid/person/:pid
if (segments.length >= 4 &&
segments[0] == 'family' &&
segments[2] == 'person') {
final fid = segments[1];
final pid = segments[3];
final family = Families.family(fid);
final person = family.person(pid);
final loc = '/family/$fid/person/$pid';
final page = MaterialPage<PersonPage>(
key: ValueKey(person),
child: PersonPage(family: family, person: person),
);
locPages[loc] = page;
}
// if we haven't found any matching routes OR if the last route doesn't
// match exactly, then we haven't got a valid stack of pages; the latter
// allows '/' to match as part of a stack of pages but to fail on
// '/nonsense'
if (locPages.isEmpty ||
locPages.keys.last.toString().toLowerCase() !=
location.toLowerCase()) {
throw Exception('page not found: $location');
}
} on Exception catch (ex) {
locPages.clear();
final loc = location;
final page = MaterialPage<Four04Page>(
key: const ValueKey('ErrorPage'),
child: Four04Page(message: ex.toString()),
);
locPages[loc] = page;
}
return Navigator(
pages: locPages.values.toList(),
onPopPage: (route, dynamic result) {
if (!route.didPop(result)) return false;
// remove the route for the page we're showing and go to the next
// location up
locPages.remove(locPages.keys.last);
_router.go(locPages.keys.last);
return true;
},
);
}
}
There's a lot going on here, but it fundamentally boils down to 3 things:
- Matching portions of the location to instances of the app's pages using manually parsed URI segments for arguments. This mapping is kept in an ordered map so it can be used as a stack of location=>page mappings.
- Providing an implementation of
onPopPage
that will translateNavigation.pop
to use the location=>page mappings to navigate to the previous page on the stack. - Show an error page if any of that fails.
This is the basic policy that the GoRouter
itself implements, although in a
simplified form w/o features like route name patterns, redirection or query
parameters.
Examples #
You can see the go_router in action via the following examples:
main.dart
: define a basic routing policy using a set of declarativeGoRoute
objectsurl_strategy.dart
: turn off the # in the Flutter web URLconditional.dart
: provide different routes based on changing app stateredirection.dart
: redirect one route to another based on changing app statequery_params.dart
: optional query parameters will be passed to all page buildersbuilder.dart
: define routing policy by providing a custom builder
You can run these examples from the command line like so:
$ flutter run example/lib/routes.dart
Or, if you're using Visual Studio Code, a launch.json
file has been provided with these examples configured.
Issues #
Do you have an issue with or feature request for go_router? Log it on the issue tracker.