Efficient Type-safe Navigation Using go_router
Tags: flutter
Published: 2023-11-04 16:17:03

In this article, I will share how I recently used [go_router](https://pub.dev/packages/go_router) for making type-safe routes for navigation. If you've been using or used `go_router`, one could argue this is one of the better ways to handle navigation in your flutter application.

If this is your first time, hearing of `go_router`, I will explain a bit what it is about. `go_router`, is a declarative routing package that uses url-based API for navigating between different screens. This means the screens have to be declared first using url based patterns before navigation can happen.


## Setup

So let us get right into it, first I will add `go_router` to my `pubspec.yaml`.
```yaml
flutter pub add go_router
```

### Route configuration
For this example, I will be using a Todo app that will have three screens,

- TodoListScreen
- AddTodoScreen
- TodoDetailScreen

Then I will create my router configuration

```dart
final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: '/',
  routes: [],
);
```
Next, I will add the screens to my `go_router` configuration as a `GoRoute`, like so

```dart
final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: '/todo-list',
  routes: [
    GoRoute(
      path: '/todo-list',
      builder: (context, state) => const TodoListScreen(),
    ),
    GoRoute(
      path: '/add-todo',
      builder: (context, state) => const AddTodoScreen(),
    ),
    GoRoute(
      path: '/todo-detail',
      builder: (context, state) {
        final extra = state.extra as Map<String, dynamic>;
        return const TodoDetailScreen(
          todo: extra['todo']! as Todo,
          userId: extra['userId']! as String,
        );
      },
    ),
  ],
);
```
We set the `initialLocation` to the same path as the route of the `TodoListScreen`, because we want that to be the first screen that is navigated to. The final thing for our setup would be to add the `go_router` configuration to my `main.dart` which hosts the `MaterialApp`.

```dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Todo App',
      routerConfig: router,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
    );
  }
}
```

### Basic navigation

Now that this has been setup, we can navigate to `AddTodoScreen`, by doing
```dart
context.push('/add-todo');
```
or 
```dart
GoRouter.of(context).push('/add-todo');
```
Both code samples do the same thing, the former is an `extension` method that called the latter under the hood.


## Need for Type-safe

So everything looks fine and you might be wondering why is there need for type-safe routes. 
Using a String for our paths makes this error-prone because it can be misspelt and it wouldn't be a compile-time error, instead we would see the error at runtime, which isn't ideal. This happens more often in medium to large production apps where a screen is navigated to from different places in the app.
Also when we pass the extra for `TodoDetailScreen`, we can't ensure for certainty that the data we pass is going to be what we want.


## Making it Type-safe

We can do this by using enums. Using enums would be to ensure we use exactly the same route names throughout our app, this will make our code easier and safer to use.

```dart
enum AppRouteInfo {
  todoList(
    path: '/todo-list',
    name: 'todoList',
  ),
  addTodo(
    path: '/add-todo',
    name: 'addTodo',
  ),
  todoDetail(
    path: '/todo-detail',
    name: 'todoDetail',
  );

  final String name;
  final String path;

  const AppRouteInfo({
    required this.name,
    required this.path,
  });
}
```

So we created an enum that requires a `name` and a `path`, this ensures our string paths are set in one place and makes it very easy to refactor and use without any worry of runtime errors from misspelt route paths.

Next we would be creating extension methods that will handle all of our navigation, the idea is to make this the only place in our app that uses `go_router` directly. This way we can keep our navigation in a separate module/package and let other modules depend on it (If you are using the monorepo approach, this might be a good way to start).

```dart
extension RouteMethods on BuildContext {
  void navigateWithArgs<T extends Object>(AppRouteInfo routeInfo, T args) {
    pushNamed(routeInfo.name, extra: args);
  }

  void navigate(AppRouteInfo routeInfo) {
    pushNamed(routeInfo.name);
  }
}
```

In our extension, we have two methods, one to navigate with arguments and the other to navigate without.

Then we will modify our router configuration to use our enum.

```dart
final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: AppRouteInfo.todoList.path,
  routes: [
    GoRoute(
      name: AppRouteInfo.todoList.name,
      path: AppRouteInfo.todoList.path,
      builder: (context, state) => const TodoListScreen(),
    ),
    GoRoute(
      name: AppRouteInfo.addTodo.name,
      path: AppRouteInfo.addTodo.path,
      builder: (context, state) => const AddTodoScreen(),
    ),
    GoRoute(
      name: AppRouteInfo.todoDetail.name,
      path: AppRouteInfo.todoDetail.path,
      builder: (context, state) {
        final args = state.extra as TodoDetailScreenArgs;
        return TodoDetailScreen(todoDetailScreenArgs: args);
      },
    ),
  ],
);
```

Then we also modify our navigation code using the extension route methods.

```dart
  void _navigateToAddTodoScreen() {
    context.navigate(AppRouteInfo.addTodo);
  }

  void _navigateToTodoDetailScreen(TodoDetailScreenArgs args) {
    context.navigateWithArgs<TodoDetailScreenArgs>(
      AppRouteInfo.todoDetail,
      args,
    );
  }
```

With these changes we've implemented our code is safer and more convenient to use.

If you found this helpful, like and share and feel free to drop suggestions and feedback.