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
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: [],
);
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,
);
},
),
],
);
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,
),
);
}
}
Basic navigation
Now that this has been setup, we can navigate to AddTodoScreen
, by doing
context.push('/add-todo');
or
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.
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).
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.
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.
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.
Top comments (0)