The Ultimate Guide to GoRouter: Navigation in Flutter Apps Part -1 (Go Router Setup, Declarative Routing, Type Safety , Path and Query Params)
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:
- GoRouter allows you to define routes in a declarative manner, which means all routing logic is centralized and not scattered throughout the application. This makes it easier to understand the navigation flow and modify it.
Built-in Deep Linking Support:
- Handling deep links is more straightforward with GoRouter, as it seamlessly integrates deep linking into its navigation system. This simplifies the process of linking to specific screens from external sources.
Type Safety:
- GoRouter supports strong typing for route parameters, reducing runtime errors and enhancing code reliability. This strong typing ensures that parameters required by different screens are correctly passed and interpreted.
Simplified Handling of Nested Routes:
- Managing nested routes (routes within routes) is much easier with GoRouter. This is particularly useful in complex applications with multiple layers of navigation, such as tabs within stacks.
Redirection and Guards:
- GoRouter provides built-in support for redirection and guards. Guards are useful for implementing requirements like user authentication before accessing certain parts of an app. Redirection helps in controlling the navigation flow based on specific conditions (e.g., redirecting to a login screen if the user is not authenticated).
Error Handling:
- It offers robust error handling capabilities, allowing developers to manage unknown routes or errors within the routing system effectively.
Programmatic and Declarative Navigation:
- While GoRouter is primarily declarative, it also supports programmatic navigation where necessary, giving developers the best of both worlds.
Performance:
- With its efficient management of route states and changes, GoRouter can lead to better performance in complex applications, where managing the navigation stack can otherwise become.
Starter Code:
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
Setup Declarative Routing:
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(),
),
],
);
}
Explanation:
GoRouter Initialization: We create a
static final
instance ofGoRouter
namedrouter
. 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 ofHomePage
.
UsingAppRouter
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
Type Safety and Parameters. Part -1 (Path Parameters)
Example 1: DetailsPage
Let'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.
Step 1: Update theDetailsPage
Widget
First, 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)),
),
);
}
}
Step 2: Add the Route inAppRouter
Now, 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');
Example 2: ProfilePage
Accessing from URL
The 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.
Step 1: Update the ProfilePage Widget
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'),
),
);
}
}
Step 2: Configure the Route in AppRouter
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 toProfilePage
is always an integer, maintaining type safety.Error Handling:The fallback to
0
if parsing fails is a simple error handling mechanism.
Step 3: Testing the Setup
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
- Add Web Platform to our existing Flutter Project
flutter create --platforms web .
- Enable Web Support. Just to be safe restart your editor
flutter config --enable-web
- Run the project in chrome
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
Type Safety and Parameters. Part -2 (Query 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.
Step 1: Define the Screen That Will Use Query Parameters
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'),
),
);
}
}
Step 2: Setup the Route with Query Parameters inAppRouter
Next, 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
Bonus Sections
Bonus 1: Setting an Initial Location
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 route
r:
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
Bonus -2: Named Routes
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
Bonus 3: Named Routes with Path Parameters
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
Bonus 4: Named Routes with Query Parameters
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
Remaining Parts:
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