# Routing Overview

> Navigation best practices.

- Source: https://engineering.verygood.ventures/development/ui/navigation/

---

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](https://pub.dev/packages/go_router) package for handling navigation needs, as it provides a robust and flexible solution for managing routes.

### GoRouter

[GoRouter](https://pub.dev/packages/go_router) is a popular routing package that is maintained by the Flutter team. It is built on top of the [Navigator 2.0](https://docs.flutter.dev/ui/navigation#using-router-and-navigator-together) 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.

Good ✅:
```txt
/
/flutter
/flutter/news
/flutter/chat
/android
/android/news
/android/chat
```

Bad ❗️:
```txt
/
/flutter
/flutter-news
/flutter-chat
/android
/android-news
/android-chat
```

:::note
Not only does using sub-routes make the path more readable, it also ensures that the app can navigate backwards correctly from the child pages.
:::

### Use type-safe routes

GoRouter allows you to define [type-safe routes](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html). 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.

Good ✅:
```dart
@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,
    );
  }
}
```

Bad ❗️:
```dart
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:

```dart
const CategoriesPageRoute(size: 'small', color: 'blue').go(context);
```

:::note
When needing to route to a page from a deep link, it is still possible to use the route's name/path and query parameters. However, using type-safe routes is recommended for navigating within the app. If your deep links are coming from an external source, choose the method that best fits your needs.

For reasons listed in the [Prefer navigating by name over path](#prefer-navigating-by-name-over-path) section below, navigating by name is preferred over navigating by path.

Navigating by name:
```dart
context.goNamed('categories', queryParameters: {'size': 'small', 'color': 'blue'})
```

Navigating by path:
```dart
context.go('/categories?size=small&color=blue');
```

:::

### 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.

:::note
It is possible, however, to update the path in the URL address bar when using `push` by adding the following:

```dart
GoRouter.optionURLReflectsImperativeAPIs = true;
```

Note that we do not recommend modifying the behavior of `push` in this way unless you are in the process of [migrating](https://docs.google.com/document/d/1VCuB85D5kYxPR3qYOjVmw8boAGKb7k62heFyfFHTOvw/edit) to GoRouter 8.0.0.

For more information on the differences between `go` and `push`, read this [Code with Andrea article](https://codewithandrea.com/articles/flutter-navigation-gorouter-go-vs-push/).
:::

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.

:::note
In a Flutter web app, the browser's back button will still be enabled as long as there are pages on the navigation stack, regardless of the navigation method that is used. Using redirects correctly will help ensure that the back button functions according to your app's navigation structure.
:::

#### 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.

Good ✅:
```txt
/user/update-address
```

Bad ❗️:
```txt
/user/update_address
/user/updateAddress
```

:::note
For a full list of URL structure best practices, take a look at this [document](https://developers.google.com/search/docs/crawling-indexing/url-structure) from Google.
:::

#### 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.

```dart
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`.

```dart
@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.

```dart
@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.

Good ✅:
```dart
context.goNamed('flutterNews');
```

Bad ❗️:
```dart
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.

```txt
/article/whats-new-in-flutter
```

To navigate to the details page of a particular article, the `GoRoute` would look like this:

```dart
// ...
@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:

```dart
FlutterArticlePageRoute(id: article.id).go(context);
```

#### Query parameters

Use query parameters when filtering or sorting resources.

```txt
/flutter/articles?date=07162024&category=all
```

To navigate to a page of filtered articles, the `GoRoute` would look like this:

```dart
// ...
@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,
    );
  }
}
// ...
```

:::note
Unlike path parameters, query parameters do not have to be defined in your route path.
:::

To navigate to the list of filtered articles:

```dart
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.

:::caution
The `extra` option used during navigation does not work on the web and cannot be used for deep linking, so we do not recommend using it.
:::

Bad ❗️:
```dart
@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);
  }
}
```

```dart
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.

Good ✅:
```dart
FlutterArticlePageRoute(id: state.article.id).go(context);
```

```dart
@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);
  }
}
```

:::note
This does not necessarily mean that you have to make another network request to fetch the article information if you already have it. You may need to refactor your repository layer to retrieve the article information from the cache if the data has already been fetched, otherwise make the request to fetch the article information.
:::

### 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.

```dart
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,
    )
  }
}
```

```dart
@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();
  }
}
```

:::note
Redirects on the parent routes are executed first. This is another reason why it is important to organize your URL hierarchy in a way where one redirect on a parent route can apply to all of the sub-routes.
:::

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.

```dart
@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();
  }
}
```
