Skip to content

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

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',
)
@immutable
class 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,
);
}
}

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

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',
)
@immutable
class 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');

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',
)
@immutable
class 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',
)
@immutable
class 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',
)
@immutable
class 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',
)
@immutable
class 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: '/',
)
@immutable
class HomePageRoute extends GoRouteData {
const HomePageRoute();
@override
Widget build(context, state) {
return HomePage();
}
}
@TypedGoRoute<SignInPageRoute>(
name: 'signIn',
path: '/sign-in',
)
@immutable
class 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',
),
],
)
@immutable
class 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;
}
}
@immutable
class PremiumShowsPageRoute extends GoRouteData {
const PremiumShowsPageRoute();
@override
Widget build(context, state) {
return PremiumShowsPage();
}
}
@immutable
class PremiumMerchPageRoute extends GoRouteData {
const PremiumMerchPageRoute();
@override
Widget build(context, state) {
return PremiumMerchPage();
}
}
@TypedGoRoute<RestrictedPageRoute>(
name: 'restricted',
path: '/restricted',
)
@immutable
class RestrictedPageRoute extends GoRouteData {
const RestrictedPageRoute();
@override
Widget build(context, state) {
return RestrictedPage();
}
}