DEV Community

Cover image for Efficient Type-safe Navigation Using go_router
Ayevbeosa Iyamu
Ayevbeosa Iyamu

Posted on

Efficient Type-safe Navigation Using go_router

In this article, I will share how I recently used 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.

flutter pub add go_router
Enter fullscreen mode Exit fullscreen mode

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

final router = GoRouter(
  debugLogDiagnostics: true,
  initialLocation: '/',
  routes: [],
);
Enter fullscreen mode Exit fullscreen mode

Next, I will add the screens to my go_router configuration as a GoRoute, like so

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,
        );
      },
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

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.

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,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Basic navigation

Now that this has been setup, we can navigate to AddTodoScreen, by doing

context.push('/add-todo');
Enter fullscreen mode Exit fullscreen mode

or

GoRouter.of(context).push('/add-todo');
Enter fullscreen mode Exit fullscreen mode

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.

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
      },
    ),
  ],
);
Enter fullscreen mode Exit fullscreen mode

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

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

  void _navigateToTodoDetailScreen(TodoDetailScreenArgs args) {
    context.navigateWithArgs<TodoDetailScreenArgs>(
      AppRouteInfo.todoDetail,
      args,
    );
  }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)