Flutter Navigation: Mastering go_router, Deep Linking, and Passing Data Between Screens

Navigation is one of the core aspects of building any mobile application. With Flutter, you have multiple ways to implement navigation, ranging from simple screen transitions to advanced techniques like deep linking. In this guide, we’ll explore how to use the go_router
package for navigation, how to set up deep linking, and how to pass data between screens, including practical examples and advanced use cases to ensure you understand how to create seamless user experiences.
1. Introduction to go_router
The go_router
package is a powerful, declarative routing solution for Flutter. It is particularly well-suited for managing complex navigation flows, deep linking, and redirections. The go_router
package provides a simple and flexible way to define routes, making it easier to maintain and extend your navigation logic. Additionally, go_router
provides better integration with web and desktop environments, making it ideal for cross-platform applications.
Setting Up go_router
To use go_router
in your Flutter project, add it as a dependency in your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
go_router: ^6.0.0
Run flutter pub get
to install the package.
Next, import the package in your main file and configure the router:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.params['id'];
return DetailsScreen(id: id!);
},
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'Flutter Navigation with go_router',
);
}
}
In this example, we have defined two routes:
- HomeScreen: The main page of the app.
- DetailsScreen: A screen that takes an
id
parameter, allowing data to be passed between screens.
By using go_router
, we simplify the process of defining routes and handling navigation logic, resulting in more maintainable and organized code.
2. Deep Linking with go_router
Deep linking allows users to be directed to specific content or screens in your app via URLs. This feature is especially useful when integrating with external systems, such as opening a product page directly from a web link. Deep linking can significantly improve user engagement and retention by providing a seamless experience that takes users exactly where they need to go.
Configuring Deep Linking
To implement deep linking with go_router
, you'll need to define the route that can handle specific URL patterns.
Here’s an example of how to configure deep linking with go_router
:
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/product/:productId',
builder: (context, state) {
final productId = state.params['productId'];
return ProductScreen(productId: productId!);
},
),
],
urlPathStrategy: UrlPathStrategy.path,
);
The urlPathStrategy
is used to specify how URLs are managed in your app. For example, with UrlPathStrategy.path
, URLs are clean and look similar to those of a traditional website (https://myapp.com/product/123
). This approach is ideal for applications that need a web-like URL structure for easier navigation and search engine indexing.
Handling Incoming Links
To handle deep links that open the app, you can use the Firebase Dynamic Links
or uni_links
package to catch incoming URLs and navigate accordingly.
Example: Using uni_links
to handle incoming deep links.
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'package:go_router/go_router.dart';
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late GoRouter _router;
@override
void initState() {
super.initState();
_initDeepLinkListener();
}
void _initDeepLinkListener() async {
getUriLinksStream().listen((Uri? uri) {
if (uri != null) {
_router.go(uri.path);
}
}, onError: (err) {
// Handle errors
});
}
@override
Widget build(BuildContext context) {
_router = GoRouter(
routes: [
GoRoute(path: '/', builder: (context, state) => HomeScreen()),
GoRoute(path: '/product/:productId', builder: (context, state) => ProductScreen(productId: state.params['productId']!)),
],
);
return MaterialApp.router(routerConfig: _router);
}
}
In this example, we use getUriLinksStream()
from the uni_links
package to listen for incoming links. When a link is received, the app navigates to the corresponding route using _router.go(uri.path)
. This allows users to enter the app at any point that matches the deep link URL, creating a smooth experience that feels cohesive and intuitive.
3. Passing Data Between Screens
Passing data between screens is a common requirement in mobile apps. With go_router
, you can pass data through route parameters or use query parameters.
Passing Data with Route Parameters
Route parameters are an effective way to pass data between screens in Flutter. Here’s how you can pass and access data using go_router
:
Example: Passing an id
parameter to the details screen.
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.params['id'];
return DetailsScreen(id: id!);
},
);
In the DetailsScreen
, the id
parameter can be used to fetch relevant data:
class DetailsScreen extends StatelessWidget {
final String id;
DetailsScreen({required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details for Item $id')),
body: Center(
child: Text('Displaying details for item with ID: $id'),
),
);
}
}
This approach ensures that you can easily pass identifiers between screens, allowing you to load and display data dynamically.
Passing Data with Query Parameters
Sometimes, instead of including data in the path, it is more appropriate to use query parameters. Query parameters can be especially useful for filtering content, performing searches, or providing optional information.
Example: Passing query parameters to a screen.
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.queryParams['query'] ?? '';
return SearchScreen(query: query);
},
);
// Navigate to the search page with a query parameter
goRouter.go('/search?query=flutter');
In this example, a search term is passed via a query parameter, which is accessed in the SearchScreen
.
class SearchScreen extends StatelessWidget {
final String query;
SearchScreen({required this.query});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search Results')),
body: Center(
child: Text('Results for: $query'),
),
);
}
}
Query parameters can be used for a wide range of purposes, such as sorting or filtering data, and they make your app’s navigation more dynamic and user-friendly.
4. Advanced Use Cases
Nested Navigation
Sometimes, your app may require nested navigation. This occurs when you have multiple navigators, such as a bottom navigation bar or tab view, each with its own stack of screens. Nested navigation allows users to switch between different sections of your app without losing their place within each section.
Example: Using nested routes with go_router
.
GoRoute(
path: '/profile',
builder: (context, state) => ProfileScreen(),
routes: [
GoRoute(
path: 'edit',
builder: (context, state) => EditProfileScreen(),
),
],
);
In this example, /profile
and /profile/edit
represent a nested navigation structure where the EditProfileScreen
is a child route of the ProfileScreen
. This is useful when you want to allow users to navigate within a specific section without affecting the overall app's navigation stack.
Redirections
go_router
also supports redirections, which is useful for handling scenarios like authentication checks or redirecting to a default screen based on conditions. Redirections can improve user experience by ensuring they always see appropriate content based on their authentication status or preferences.
Example: Redirecting unauthenticated users to a login page.
GoRouter(
redirect: (context, state) {
final loggedIn = false; // Replace with actual authentication check
final loggingIn = state.location == '/login';
if (!loggedIn && !loggingIn) {
return '/login';
}
return null;
},
routes: [
GoRoute(path: '/', builder: (context, state) => HomeScreen()),
GoRoute(path: '/login', builder: (context, state) => LoginScreen()),
],
);
In this example, unauthenticated users trying to access any page other than /login
will be redirected to the login screen. This ensures that users cannot access restricted content without proper authorization.
Summary
With Flutter, navigation can be managed in a multitude of ways depending on the complexity and requirements of your app. Using go_router
offers a powerful and flexible solution for implementing straightforward navigation, handling deep linking, and passing data effectively between screens. Deep linking enhances the user experience by allowing direct access to specific content, while route and query parameters make your app's navigation more dynamic and flexible.