Routing Overview
Navigation is a crucial component of any app. A declarative routing structure is essential for building scalable apps that function seamlessly on both mobile and web platforms. At VGV, we recommend using the GoRouter package for handling navigation needs, as it provides a robust and flexible solution for managing routes.
GoRouter
GoRouter is a popular routing package that is maintained by the Flutter team. It is built on top of the Navigator 2.0 API and reduces much of the boilerplate code that is required for even simple navigation. It is a declarative routing package with a URL-based API that supports parsing path and query parameters, redirection, sub-routes, and multiple navigators. Additionally, GoRouter works well for both mobile and web apps.
Configuration
To enable deep linking in your app (such as redirecting to a login page or other features), routing must be carefully configured to properly support backwards navigation.
Structure your routes in a way that makes logical sense. Avoid placing all of your routes on the root path. Instead, use sub-routes.
//flutter/flutter/news/flutter/chat/android/android/news/android/chat
//flutter/flutter-news/flutter-chat/android/android-news/android-chat
Use type-safe routes
GoRouter allows you to define type-safe routes. When routes are type-safe, you no longer have to worry about typos and casting your route’s path and query parameters to the correct type.
@TypedGoRoute<CategoriesPageRoute>( name: 'categories', path: '/categories',)@immutableclass CategoriesPageRoute extends GoRouteData { const CategoriesPageRoute({ this.size, this.color, });
final String? size; final String? color;
@override Widget build(context, state) { return CategoriesPage( size: size, color: color, ); }}
GoRoute( name: 'categories', path: '/categories', builder: (context, state) { final size = state.uri.queryParameters['size']; final color = state.uri.queryParameters['category']; return CategoriesPage(size: size, color: color); })
Navigating to the categories
page using the type-safe route is as simple as calling:
const CategoriesPageRoute(size: 'small', color: 'blue').go(context);
Prefer go
over push
methods
GoRouter offers multiple ways to navigate to a route, such as pushing every route onto the stack and navigating to a route’s path.
When possible, use GoRouter’s go
methods for navigation. Calling go
pushes a new route onto the navigation stack according to your route’s path and updates the path in your browser’s URL address bar (if on web).
Use the push
method for navigation when you are expecting to receive data from a route when it is popped. Popping with data is a common scenario when pushing a dialog onto the stack which collects input from the user. Since you will never be expected to route the user directly to the dialog from a deep link, using push
prevents the address bar from updating the route.
Using go
will ensure that the back button in your app’s AppBar
will display when the current route has a parent that it can navigate backwards to. Root paths will not display a back button in their AppBar
. For example, /flutter/news
would display a back arrow in the AppBar
to navigate back to /flutter
, but /flutter
would not not display a back button. Using sub-routes correctly removes the need to manually handle the back button functionality.
Use hyphens for separating words in a URL
Mobile app users will likely never see your route’s path, but web app users can easily view it in the browser’s URL address bar. Your routing structure should be consistent and defined with the web in mind. Not only does this make your paths easier to read, it allows you the option of deploying your mobile app to the web without any routing changes needed.
/user/update-address
/user/update_address/user/updateAddress
Prefer navigating by name over path
If you’re using GoRoute’s type-safe routes, navigate using the go
extension method that was generated for your route.
FlutterNewsPageRoute().go(context);
If a route to a page is given to you from an external source, such as a push notification, to deep link to a specific page within your app, GoRouter allows you to navigate to a route by its name or by its path.
Because your app’s structure and paths can change over time, we recommend routing by name to avoid potential issues of a route’s path getting out of sync.
Consider this situation: An app has a route defined with the path /flutter-news
for the FlutterNewsPage
.
@TypedGoRoute<FlutterNewsPageRoute>( name: 'flutterNews', path: '/flutter-news',)@immutableclass FlutterNewsPageRoute extends GoRouteData { @override Widget build(context, state) { return const FlutterNewsPage(); }}
Later, the pages in the app were reorganized and the path to the FlutterNewsPage
has changed.
@TypedGoRoute<TechnologyPageRoute>( name: 'technology', path: '/technology', routes: [ TypedGoRoute<FlutterPageRoute>( name: 'flutter', path: 'flutter', routes: [ TypedGoRoute<FlutterNewsPageRoute>( name: 'flutterNews', path: 'news', ), ], ), ],)
If the app was relying on the path
to navigate the user to the FlutterNewsPage
and the deep link path from the external source didn’t match the route’s path, the route would not be found. However, when relying on the route name
, navigation would work in either situation.
Extension methods
GoRouter provides extension methods on BuildContext
to simplify navigation. For consistency, use the extension method over the longer GoRouter
methods since they are functionally equivalent.
context.goNamed('flutterNews');
GoRouter.of(context).goNamed('flutterNews');
Navigating with parameters
Many times when navigating, you need to pass data from one page to another. GoRouter makes this easy by providing multiple ways to accomplish this: path parameters, query parameters, and an extra parameter.
Path parameters
Use path parameters when identifying a specific resource.
/article/whats-new-in-flutter
To navigate to the details page of a particular article, the GoRoute
would look like this:
// ...@TypedGoRoute<FlutterArticlePageRoute>( name: 'flutterArticle', path: 'article/:id',)@immutableclass FlutterArticlePageRoute extends GoRouteData { const FlutterArticlePageRoute({ required this.id, });
final String id;
@override Widget build(context, state) { return FlutterArticlePage(id: id); }}// ...
Navigating to that page with the article id is as simple as providing the article id to the FlutterArticlePageRoute
’s constructor:
FlutterArticlePageRoute(id: article.id).go(context);
Query parameters
Use query parameters when filtering or sorting resources.
/flutter/articles?date=07162024&category=all
To navigate to a page of filtered articles, the GoRoute
would look like this:
// ...@TypedGoRoute<FlutterArticlesPageRoute>( name: 'flutterArticles', path: 'articles',)@immutableclass FlutterArticlesPageRoute extends GoRouteData { const FlutterArticlesPageRoute({ this.date, this.category, });
final String? date; final String? category;
@override Widget build(context, state) { return FlutterArticlesPage( date: date, category: category, ); }}// ...
To navigate to the list of filtered articles:
FlutterArticlesPageRoute(date: state.date, category: state.category).go(context);
Extra parameter
GoRouter has the ability to pass objects from one page to another. Most of the time, however, we avoid using the extra
object when navigating to a new route.
@TypedGoRoute<FlutterArticlePageRoute>( name: 'flutterArticle', path: 'article',)@immutableclass FlutterArticlePageRoute extends GoRouteData { const FlutterArticlePageRoute({ required this.article, });
final Article article;
@override Widget build(context, state) { return FlutterArticlePage(article: article); }}
FlutterArticlePageRoute(article: article).go(context);
In this example, we are passing the article
object to the article details page. If your app is designed to only work on mobile and there are no plans of deep linking to the articles details page, then this is fine. But, if the requirements change and now you want to support the web or deep link users directly to the details of a particular article, changes will need to be made. Instead, pass the identifier of the article as a path parameter and fetch the article information from inside of your article details page.
FlutterArticlePageRoute(id: state.article.id).go(context);
@TypedGoRoute<FlutterArticlePageRoute>( name: 'flutterArticle', path: 'article/:id',)@immutableclass FlutterArticlePageRoute extends GoRouteData { const CategoriesPageRoute({ required this.id, });
final String id;
@override Widget build(context, state) { return FlutterArticlePage(id: id); }}
Redirects
Sometimes you need to redirect users to a different location in the app. For example: only signed-in users can access parts of your app. If the user isn’t signed-in, you want to redirect the user to the sign in page. Fortunately, GoRouter makes this very easy and redirects can be done at the root and sub-route level.
class AppRouter { AppRouter({ required GlobalKey<NavigatorState> navigatorKey, }) { _goRouter = _routes( navigatorKey, ); }
late final GoRouter _goRouter;
GoRouter get routes => _goRouter;
GoRouter _routes( GlobalKey<NavigatorState> navigatorKey, ) { return GoRouter( initialLocation: '/', navigatorKey: navigatorKey, redirect: (context, state) { final status == context.read<AppBloc>().state.status;
if (status == AppStatus.unauthenticated) { return SignInPageRoute().location; }
return null; }, routes: $appRoutes, ) }}
@TypedGoRoute<HomePageRoute>( name: 'home', path: '/',)@immutableclass HomePageRoute extends GoRouteData { const HomePageRoute();
@override Widget build(context, state) { return HomePage(); }}
@TypedGoRoute<SignInPageRoute>( name: 'signIn', path: '/sign-in',)@immutableclass SignInPageRoute extends GoRouteData { const SignInPageRoute();
@override Widget build(context, state) { return SignInPage(); }}
In this example, the user is redirected to the restricted
page if the user’s status isn’t premium
and tries to access /premium
, /premium/show
, or /premium/merch
. Having shows
and merch
as sub-routes avoids having to add redirect logic to each route.
@TypedGoRoute<PremiumPageRoute>( name: 'premium', path: '/premium', routes: [ TypedGoRoute<PremiumShowsPageRoute>( name: 'premiumShows', path: 'shows', ), TypedGoRoute<PremiumMerchPageRoute>( name: 'premiumMerch', path: 'merch', ), ],)@immutableclass PremiumPageRoute extends GoRouteData { const PremiumPageRoute();
@override Widget build(context, state) { return PremiumPage(); }
@override String? redirect(context, state) { final status == context.read<AppBloc>().state.user.status;
if (status != UserStatus.premium) { return RestrictedPageRoute().location; }
return null; }}
@immutableclass PremiumShowsPageRoute extends GoRouteData { const PremiumShowsPageRoute();
@override Widget build(context, state) { return PremiumShowsPage(); }}
@immutableclass PremiumMerchPageRoute extends GoRouteData { const PremiumMerchPageRoute();
@override Widget build(context, state) { return PremiumMerchPage(); }}
@TypedGoRoute<RestrictedPageRoute>( name: 'restricted', path: '/restricted',)@immutableclass RestrictedPageRoute extends GoRouteData { const RestrictedPageRoute();
@override Widget build(context, state) { return RestrictedPage(); }}