The Ultimate Guide to GoRouter: Navigation in Flutter Apps Part -1 (Go Router Setup, Declarative Routing, Type Safety , Path and Query Params)

Search for a command to run...

No comments yet. Be the first to comment.
While I generally write about Flutter. This problem has been bugging me for a while and there seem to be limited resources to find a solution. So for anyone who is looking for a solution. Here you go: Type edge:flags into the address bar and open it...
In this tutorial, we’ll go through covering interceptor, which is a very crucial feature of Dio. You can find the entire source code for the project here: dio_tasker Understanding Interceptors: What is an interceptor? An interceptor in Dio is a powe...

In this part, we’ll enhance our Task Manager app by parsing JSON responses and handling different types of errors. We’ll use Dio’s built-in error handling mechanisms. You can find the source code of the entire app here: dio_tasker Parsing JSON Respon...

While building apps in Flutter is extremely fluid and wonderfully intuitive. We obviously know that we can’t build the next TikTok or Twitter by only building offline apps. Which is why we absolutely need internet capabilities in our Flutter apps. Th...

Understanding when to use setState in Flutter is crucial for managing our app’s state effectively. Here’s a detailed guide: When to use setState ? Updating the UI: Use setState when we need to update the UI in response to changes in the internal sta...

As a Flutter developer, you're likely no stranger to the importance of navigation in your app. Whether you're building a simple to-do list or a complex e-commerce platform, navigation is a crucial aspect of providing a seamless user experience.
In this article, we'll dive into the world of GoRouter, a powerful navigation library for Flutter that makes it easy to manage your app's navigation flow.
What is GoRouter?
GoRouter is an open-source navigation library for Flutter that provides a simple and efficient way to manage your app's navigation. Developed by the Flutter team, GoRouter is designed to simplify the process of navigating between screens, handling route changes, and managing the app's navigation stack.
Why Use GoRouter? All the Features comprehensively: We are going to cover every single one in this article.
Declarative Routing:
Built-in Deep Linking Support:
Type Safety:
Simplified Handling of Nested Routes:
Redirection and Guards:
Error Handling:
Programmatic and Declarative Navigation:
Performance:
This is the starter code of the app: https://github.com/khkred/flutter_comp_go_router/tree/starter-code
This is the current tree of the screens inside the starter code
lib/screens/
├── dashboard_page.dart
├── deep_link_page.dart
├── details_page.dart
├── error_page.dart
├── home_page.dart
├── login_page.dart
├── profile_page.dart
└── settings_page.dart
1 directory, 8 files
Each screen will have a specific purpose in the future:
HomePage - Declarative Routing
Details Page - Type Safety and Parameters
Settings Page - Nested Routes
Profile Page - Type Safety and Parameters
Login Page - Redirection and Guards
Dashboard Page - Nested Routes
Error Page - Error Handling
Deep Link Page - Built-in Deep Linking Support
First we have to setup the Go Router in our app. Our GoRouter package is already added into the Starter Code. But If you want to add it into your pubspec.yaml. Run the following command in your terminal:
flutter pub add go_router
Now that the GoRouter is added. Let's setup the initial navigation for GoRouter in a separate file named app_router.dart . Here is the code:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'screens/home_page.dart';
class AppRouter {
static final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) => const HomePage(),
),
],
);
}
GoRouter Initialization: We create a static final instance of GoRouter named router. This instance is initialized with a list of routes.
Route Definition: The GoRoute constructor is used to define a route.
path: This specifies the path in the URL. Here, '/' denotes the root or home route.
builder: This function returns the widget that should be displayed when navigating to this route. In this case, it returns an instance of HomePage.
AppRouter in Your Main Application:To use the AppRouter in your main application, modify your main.dart file to utilize the GoRouter instance for routing:
import 'package:flutter/material.dart';
import 'app_router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Go Router Example',
routerConfig: AppRouter.router,
);
}
}
Perfect. So now when we open the app it directly goes to the homepage:

So this covers Declarative Routing. You can find the total code here: https://github.com/khkred/flutter_comp_go_router/tree/declarative-routing
DetailsPageLet's set up the Details Page to demonstrate type safety and parameter handling using GoRouter. We'll create a route that requires a parameter, such as an ID, to fetch and display specific details on the Details Page.
DetailsPage WidgetFirst, let’s modify the DetailsPage to accept a parameter, say an item ID, which it will use to display specific content.
import 'package:flutter/material.dart';
class DetailsPage extends StatelessWidget {
final int itemId;
// Ensuring type safety with required parameters
const DetailsPage({super.key, required this.itemId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Details Page')),
body: Center(
child: Text('Displaying details for item ID: $itemId',
style: const TextStyle(fontSize: 24)),
),
);
}
}
AppRouterNow, integrate this page into your AppRouter by defining a route that includes the parameter. You’ll capture the parameter from the URL and pass it to the DetailsPage.
Update your app_router.dart file like this:
import 'package:flutter/material.dart';
import 'package:flutter_comp_go_router/screens/details_page.dart';
import 'package:go_router/go_router.dart';
import 'screens/home_page.dart';
class AppRouter {
static final GoRouter router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomePage(),
),
GoRoute(
path: 'details/:itemId',
builder: (BuildContext context, GoRouterState state) {
final params = state.pathParameters['itemId'];
final int itemId = int.tryParse(params ?? '') ?? 0;
return DetailsPage(itemId: itemId);
}),
],
);
}
Now we are adding a textfield in the home_page.dart where a user can enter a number and the app will take you the details_page.dart with that id.
Here's the relevant function in home_page.dart with navigation:
void _navigateToDetails() {
if (_detailsPageIdController.text.isNotEmpty) {
final itemId = int.tryParse(_detailsPageIdController.text);
if (itemId != null) {
//We are going to the details page with the item ID
context.go('/details/$itemId');
} else {
// Error handling for non-numeric input
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: const Text('Please enter a valid number.'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
}
}

But you cannot go back from the details page. If you want to go back then all you have to do is change
context.go('/details/$itemId');
to
context.push('/details/$itemId');

ProfilePage Accessing from URLThe ProfilePage will demonstrate how to securely pass and handle route parameters, specifically focusing on ensuring type safety, which is crucial for preventing runtime errors and bugs related to data types.
Let's define the ProfilePage to accept a user ID as a parameter and display it. This is a basic example of type safety, as the page will expect an integer and handle it appropriately. This is extremely similar to DetailsPage but we want to see how we'll access it from the URL
import 'package:flutter/material.dart';
class ProfilePage extends StatelessWidget {
final int userId;
// Ensuring type safety with required integer parameter
const ProfilePage({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile Page')),
body: Center(
child: Text('Profile for user ID: $userId'),
),
);
}
}
Now, integrate the ProfilePage into your routing setup in the AppRouter. This involves capturing a path parameter from the URL and passing it securely to the ProfilePage.
So let's go ahead and add the following Route to our routes in app_router.dart
GoRoute(
path: '/profile/:userId',
builder: (BuildContext context, GoRouterState state){
final params = state.pathParameters['userId'];
// Here we are making sure that the userId is an integer for Type Safety
final int userId = int.tryParse(params ?? '') ?? 0;
return ProfilePage(userId: userId);
}),
In this setup:
Path Parameter Handling: The route uses :userId as a path parameter. This parameter is extracted from the URL and converted to an integer (int.tryParse). This ensures that the ID passed to ProfilePage is always an integer, maintaining type safety.
Error Handling:The fallback to 0 if parsing fails is a simple error handling mechanism.
We want to test the app using URLS. So First let's add web support for our app. Since our current app only supports Android and iOS. Run the following command in our terminal
flutter create --platforms web .
flutter config --enable-web
flutter run -d chrome
Now after we run the app on Chrome. We can enter the URL

Remove the # from URL:
If you wish to eliminate the ‘#’ symbol from your URL, you can use the usePathUrlStrategy class in Flutter.
So in our main.dart we have to add the following code
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
void main(){
usePathUrlStrategy();
runApp(MyApp());
}
You can find the final source code here: https://github.com/khkred/flutter_comp_go_router/tree/type_safety_and_parameters
Definition: Query parameters are used to provide optional modifications or filter data on a given resource or page. They are not part of the URL path but are appended at the end after a ? mark.
Example:
GoRoute(
path: '/users',
builder: (context, state) => UsersScreen(filter: state.queryParameters['filter']),
),
Difference between Path Parameters and Query Parameters:
Here is a table describing the difference between the two:
| Path Parameter | Query String Parameter | |
| Syntax | :userId | ?filter=admins |
| Access | state.pathParameters['userId'] | state.uri.queryParameters['filter'] |
| Example | /users/:userId | /users?filter=admins |
| Purpose | Specify a dynamic part of the URL path | Specify a dynamic query string parameter |
| Usage | Use in path to specify a dynamic value | Use in query string to specify a dynamic value |
In summary, path parameters are used to specify dynamic parts of the URL path, while query string parameters are used to specify dynamic values in the query string.
First, let's define a Flutter screen that can accept query parameters. For example, we might create a UsersScreen that can filter users based on a query parameter named filter. Here’s a basic implementation:
import 'package:flutter/material.dart';
class UsersScreen extends StatelessWidget {
final String? filter;
const UsersScreen({super.key, this.filter});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Users Screen')),
body: Center(
// Displaying the filter if it exists
child: Text(filter != null ? 'Filtering by: $filter' : 'No filter applied'),
),
);
}
}
AppRouterNext, let's add a route to our AppRouter to handle a route that includes query parameters. Here’s how you might set it up:
// Pay Attention to this route as it has a query parameter and the filter is passed to the UsersScreen
GoRoute(path: '/users',
builder: (BuildContext context, GoRouterState state){
final String? filter = state.uri.queryParameters['filter'];
return UsersScreen(filter: filter);
}),
So this functions in the HomePage helps us navigate to the UsersPage()
void _navigateToUsers() {
final filter = _filterController.text;
if (filter.isNotEmpty) {
context.push('/users?filter=$filter');
} else {
context.push('/users');
}
}

Here's the current source code: https://github.com/khkred/flutter_comp_go_router/tree/query_parameters
If we want to set an initial location for the app regardless of the root location we can do it by adding the initiaLocation:
First let's add a sample_page.dart to our app:
class SamplePage extends StatelessWidget {
const SamplePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('This is the sample page, we got from initial Location'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
context.go('/'); // This will navigate to the home page
},
child: const Text('Go to Home Page'),
),
],
),
),
);
}
}
Now this is our updated router:
class AppRouter {
static final GoRouter router = GoRouter(
initialLocation: '/sample',
routes: [
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomePage(),
),
GoRoute(path: '/sample',
builder: (BuildContext context, GoRouterState state) => const SamplePage(),),
.........
This is what I get when I run the app right now:

As always you can find the link for the bonus code here: https://github.com/khkred/flutter_comp_go_router/tree/type_safety_and_parameters
You can set a name for any route. So that we can call it instead of calling the entire path.
For instance let's give a name to our HomePage() route.
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomePage(),
),
Now I can navigate from our SamplePage() like this:
ElevatedButton(
onPressed: () {
context.goNamed('home'); // This will navigate to the home page
},
child: const Text('Go to Home Page'),
),
You can find the code here: named route commit
For instance let's set a name for our details route:
GoRoute(
name: 'details',
path: '/details/:itemId',
...),
Now if we want to call it via name. Let's modify the _navigateToDetails() in our home_page
//Old Navigation
//context.push('/details/$itemId');
//New Navigation with named
context.pushNamed(
'details',
pathParameters: {'itemId': '$itemId'},
);
}
Here's the total code: named route with path parameters commit
Let's add a name to our UsersScreen() route too:
GoRoute(
path: '/users',
name: 'users',
...),
Now we can update the _navigateToUsers() in our HomePage like this:
//Old Query Parameter
//context.push('/users?filter=$filter');
//New named query parameters
context.pushNamed(
'users',
queryParameters: {'filter': filter},
);
Here's the code: Named Route with Query Parameters Commit
Here's the link for part 2: https://harishkunchala.com/the-ultimate-guide-to-gorouter-navigation-in-flutter-apps-part-2-nested-routers-redirect-guard-error-handling
Here's the link for part 3: https://harishkunchala.com/the-ultimate-guide-to-gorouter-navigation-in-flutter-apps-part-3-custom-transitions