Flutter navigator into router Api: GoRouter

Md. Ahsanul Haque

19 September, 2024

As our Flutter app scales, navigating through different screens can become complex. While powerful, Flutter’s default navigation tools can be cumbersome when dealing with nested routes, deep linking, and dynamic route handling. GoRouter, a powerful package, simplifies this process with a more intuitive and declarative approach, empowering developers to manage app navigation more effectively, particularly in large applications.

In this guide, we’ll explore all the features GoRouter offers and detail key concepts like push vs. go, deep linking, route guards, nested routes, and more.

What is GoRouter?

GoRouter is a third-party routing package for Flutter that provides a straightforward and flexible way to manage app navigation. It introduces a declarative API that allows us to define our routes directly in the widget tree, making navigation easier to manage, particularly in large applications. GoRouter’s ease of use makes it a comfortable and effective tool for developers.

Key Features of GoRouter
  • Declarative Routing: Routes are defined within the widget tree, making it easier to visualize and manage the navigation flow.
  • Deep Linking: Directly supports deep links, enabling us to navigate to specific pages in our app via URLs.
  • Redirection and Route Guards: Easily implement redirects and protect specific routes based on conditions like user authentication.
  • Dynamic and Nested Routes: Manage dynamic URL segments and nested routing structures effortlessly.
  • State Preservation: This feature keeps the UI state intact across route changes, essential for complex apps with persistent UI elements.
Setting Up GoRouter

Start by adding GoRouter to our pubspec.yaml:

				
					dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.2.1
				
			

Then, import GoRouter in our Dart files:

				
					import 'package:go_router/go_router.dart';
				
			
Basic Routing with GoRouter

To get started with GoRouter, we’ll need to define our routes using the GoRouter widget. Here’s an example of a simple routing setup:

				
					class RouterConstants {
 static const homePageName = 'Home';
 static const homePagePath = '/';
 static const productsPageName = 'Products';
 static const productsPagePath = '/products';
 static const productPageName = 'Product';
 static const productPagePath = '/products/product/:productId';
}

final myRouter = GoRouter(
 initialLocation: RouterConstants.homePagePath,
 debugLogDiagnostics: true,
 routes: [
   GoRoute(
     name: RouterConstants.homePageName,
     path: RouterConstants.homePagePath,
     builder: (_, __) => const HomePage(),
   ),
   GoRoute(
     name: RouterConstants.productsPageName,
     path: RouterConstants.productsPagePath,
     builder: (_, __) => const ProductsPage(),
   ),
   GoRoute(
     name: RouterConstants.productPageName,
     path: RouterConstants.productPagePath,
     builder: (_, state) => ProductPage(
       productId: state.pathParameters['productId']??'Unknown',
     ),
   ),
 ],
);
				
			

In this example:

  • GoRouter: The main routing object that defines all the routes.
  • GoRoute: Represents an individual route. We can specify the path, the widget to build, and any dynamic parameters.

Each GoRoute represents a distinct route in our application, linking a specific URL path to a corresponding widget. We use GoRoute to map URL paths, define sub-routes, handle dynamic segments, and more.

Parameters:
  • path: The URL path for this route can include dynamic segments, which are variables in the path prefixed with a colon (:).

Example: /products/product/:productId maps to a route where productId is a dynamic segment.

  • builder: A function that takes BuildContext and GoRouterState as arguments and returns the widget for the route. This function defines what should be displayed when navigating to this route.
  • name: An optional name for the route. Naming routes can help generate URLs or navigate without hardcoding paths.

Example: name: ‘Product’ 

Additional parameters in GoRoute unlock powerful and elegant features, which we’ll explore later.

Then, we will pass this router into MaterialApp as follows:

				
					return MaterialApp.router(
 debugShowCheckedModeBanner: false,
 title: 'Flutter Demo',
 theme: ThemeData(
   colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
   useMaterial3: true,
 ),
 routerConfig: myRouter,
);
				
			
Navigating Between Routes

With GoRouter, navigating between routes is straightforward. We can use the context.go() or context.push() methods to navigate:

  • context.go(RouterConstants.productsPagePath): Replaces the current route with a new one (similar to Navigator.pushReplacement).
  • context.push(RouterConstants.productPagePath): Pushes a new route onto the stack (similar to Navigator.push).

We can also navigate using the name of the route rather than the path as:

				
					context.goNamed(RouterConstants.productsPageName);
context.pushNamed(
RouterConstants.productPageName,
pathParameters: {'productId': productId},
);

				
			

PathParameters is used to pass productId into this path as a dynamic pathParameter. The product page’s full path is /products/product/:productId, where ‘:’ is used before the dynamic pathParameter productId.

Like pathParameters, there are a few more parameters in both go and push methods: queryParameters and extra.

queryParameters are key-value pairs appended to the URL after a ?. queryParameters commonly represent optional parameters like search queries, filters, or pagination. Access and use these parameters within the route’s builder to adjust the page’s content based on the provided query.

 

Example:

				
					GoRoute(
  path: '/search',
  builder: (context, state) {
    final query = state.queryParameters['q'] ?? '';
    return SearchPage(query: query);
  },
);
				
			

And the usage:

				
					context.go('/search?q=flutter'); // Navigates to SearchPage with query = "flutter"
				
			

extra allows us to pass additional data with the navigation action. This data can be of any type, including complex objects that don’t easily fit into a URL. It’s a flexible way to pass data unsuitable for pathParameters or queryParameters.

Example

				
					GoRoute(
  path: '/profile',
  builder: (context, state) {
    final user = state.extra as User;
    return ProfilePage(user: user);
  },
);
				
			

The usage is:

				
					final user = User(name: 'John Doe', age: 30);
context.push('/profile', extra: user); // Navigates to ProfilePage with the User object
				
			
When to go vs. push
  • go: We use this when we want to replace the current page, especially in scenarios like redirecting after a successful login or navigating from a splash screen to the main app.
  • push: We use this when we want to keep a history of navigation, allowing the user to go back to the previous screen, such as in a typical master-detail navigation pattern.
Handling Deep Links

GoRouter simplifies deep linking. When you open the app with a specific URL, GoRouter matches the URL to a route in your app. For example, if the app launches with myapp://details/1, GoRouter navigates directly to the DetailsPage.

To enable deep linking for Android or iOS applications, ensure our AndroidManifest.xml and Info.plist files are configured correctly for URL schemes.

Similarly, in the web application, if we hit a URL like /products/product/1, we will be taken to the product page displaying details for productId 1. Since the/products endpoint is the product section’s base path, the URL /products/product/1 indicates that the product page will appear on top of the products page, essentially overlaying it. Here, /products is the parent of the product page. 

By default, Flutter adds a hash (#) into the URL for web apps. To remove this hash from the URL, we have to add a line of code usePathUrlStrategy() into main() as below:

				
					import 'package:flutter_web_plugins/url_strategy.dart';

void main() {
 usePathUrlStrategy();
 runApp(const MyApp());
}
				
			
Route Guards and Redirection

GoRouter allows us to protect routes and redirect users based on certain conditions. For example, we suggest redirecting users to a login page if they try to access a restricted page, such as the profile page, without being authenticated.

Here’s how we can implement a simple route guard:

				
					GoRoute(
 name: RouterConstants.profilePageName,
 path: RouterConstants.profilePagePath,
 builder: (context, state) => ProfilePage(),
 redirect: (context, state) async{
   final bool isLoggedIn = await GoPreference.readValue(); // Replace with actual login check
   return isLoggedIn ? null : RouterConstants.loginPagePath;
 },
),
				
			

In this example, redirect checks the app’s state and returns a path to redirect if the condition fails. We store the isLoggedIn status using shared preferences.

We can also manage the LoggedIn state and use state management solutions like BLoC, Provider, Riverpod, or other tools. GoRouter provides a feature called refreshListenable, which allows us to listen for changes in our state and automatically trigger route reevaluation. Bypassing our state notifier (e.g., a ChangeNotifier or ValueNotifier) to refreshListenable, GoRouter will react to state changes and update the routing accordingly. Later, we will discuss this topic in another article.

Debugging our Routes

When working with GoRouter, keeping track of all the routes in our application can be challenging, especially when using fragments or dynamic segments that match only parts of a location. To simplify debugging, GoRouter offers a parameter called debugLogDiagnostics. By setting this parameter to true, we can enable detailed logging that outputs the full paths of the routes we’ve defined, making diagnosing and resolving routing issues easier.

State Preservation in GoRouter

One of GoRouter’s benefits is its ability to preserve the state of the UI across navigation events. State preservation refers to its ability to maintain the UI’s state across different navigation events, ensuring that when users navigate away from a page and return to it, the page remains in the same state as they left it. This feature is precious in complex applications where elements such as forms, scroll positions, user inputs, or any interactive components need to persist in their state.

Conclusion

GoRouter simplifies Flutter navigation, especially in complex applications. Its declarative approach, deep linking support, and powerful features like route guards and nested routing make it a go-to choice for modern Flutter apps. Adopting GoRouter can enhance our app’s navigation structure, making it more maintainable and scalable.

Whether we’re building a small app or an extensive enterprise application, GoRouter provides the tools we need to manage navigation effectively in Flutter.

We can get the entire project from git via this link: https://github.com/Ahsan-dev/gorouter.git.

Md. Ahsanul Haque

19 September, 2024