Why do we use Go Router?

EM Diya
8 min readJul 16, 2024

--

GoRouter is a flexible and declarative routing library for Flutter applications. It simplifies navigation by allowing developers to define and manage app navigation paths efficiently. It supports features like nested routes, type-safe routing, custom transitions, route guards for secure navigation, and seamless integration with state management solutions like GetX. GoRouter enhances the user experience by providing smooth transitions between screens and ensuring reliable navigation flow in Flutter apps.

1. Declarative Routing

GoRouter allows you to define your app’s navigation paths in a clear and declarative manner. Routes and nested routes can be structured hierarchically, making it easy to visualize and maintain the navigation flow of your application.

2. Flexibility

The library offers flexibility in defining and handling different navigation scenarios. You can nest routes under parent routes and configure specific behaviors for each route. This flexibility is crucial for applications with complex navigation requirements.

3. Type-Safe Routing

GoRouter supports type-safe routing, which ensures that parameters passed between routes are strongly typed. This reduces the risk of runtime errors related to incorrect type conversions, leading to more reliable and maintainable code.

4. Custom Transitions and Animations

It provides built-in support for defining custom transitions and animations between routes. Developers can create smooth and visually appealing transitions, enhancing the overall user experience of the application.

5. Route Guards

Route guards in GoRouter allow you to control access to routes based on specific conditions, such as user authentication status. This feature ensures that sensitive parts of your application are protected and navigated through securely.

6. Integration with State Management

GoRouter integrates seamlessly with popular state management solutions like GetX and others. This integration allows developers to combine powerful state management capabilities with efficient routing and navigation features in their Flutter applications.

7. Testability

The library facilitates easier testing of navigation flows within Flutter applications. Developers can mock routes and navigate programmatically during unit and integration testing, ensuring the reliability and correctness of their navigation logic.

8. Community Support and Active Development

GoRouter benefits from a growing community of Flutter developers and contributors. The library is actively maintained and updated, ensuring compatibility with the latest Flutter releases and incorporating new features and improvements over time.

Step 1: Create a New Flutter Project

Create a new Flutter project using the Flutter CLI:

flutter create go_router_getx_example
cd go_router_getx_example

Step 2: Add Dependencies

Add go_router and get to your pubspec.yaml file:

dependencies:
flutter:
sdk: flutter
go_router: ^6.0.0
get: ^4.6.5

Run flutter pub get to install the new dependencies.

Step 3: Define the Data Model

Create a file home_model.dart in the lib directory:

class HomeModel {
final String id;
final String name;
final String description;

HomeModel({required this.id, required this.name, required this.description});

factory HomeModel.fromJson(Map<String, dynamic> json) {
return HomeModel(
id: json['id'],
name: json['name'],
description: json['description'],
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
};
}
}

Step 4: Create the GetX Controller

Create a file home_controller.dart in the lib directory:

import 'package:get/get.dart';
import 'home_model.dart';

class HomeController extends GetxController {
var homeList = <HomeModel>[].obs;

@override
void onInit() {
super.onInit();
// Initialize with some data
homeList.addAll([
HomeModel(id: '1', name: 'Home 1', description: 'Description of Home 1'),
HomeModel(id: '2', name: 'Home 2', description: 'Description of Home 2'),
]);
}
}

Step 5: Set Up GoRouter with Routes

Create a file app_router.dart in the lib directory:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'home_model.dart';
import 'bottom_navigation_bar.dart';
import 'home_screen.dart';
import 'detail_home_page.dart';
import 'favorite_screen.dart';
import 'detail_favorite_screen.dart';
import 'order_screen.dart';
import 'detail_order.dart';
import 'book_screen.dart';
import 'detail_book.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'shell');

final router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/',
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
return BottomNavigationbar(child: child);
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/',
builder: (context, state) {
return const HomeScreen();
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: 'detail-homepage',
name: 'DetailHomePage',
builder: (BuildContext context, GoRouterState state) {
final list = state.extra as List<HomeModel>;
return DetailHomePage(listHome: list);
},
),
],
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/favorite',
name: 'Favorite',
builder: (BuildContext context, GoRouterState state) {
return const FavoriteScreen();
},
routes: [
GoRoute(
path: 'detail-fav',
name: 'DetailFav',
builder: (BuildContext context, GoRouterState state) {
return const DetailFavoriteScreen();
},
),
],
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/order',
builder: (BuildContext context, GoRouterState state) {
return const OrderScreen();
},
routes: [
GoRoute(
path: 'detail-order',
name: 'DetailOrder',
builder: (BuildContext context, GoRouterState state) {
return DetailOrder(
id: int.tryParse(state.queryParams['id'] ?? ''),
);
},
),
],
),
],
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/books',
builder: (BuildContext context, GoRouterState state) {
return const BookScreen();
},
routes: [
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: ':id',
builder: (BuildContext context, GoRouterState state) {
return DetailBook(
id: int.tryParse(state.params['id'] ?? ''),
);
},
),
],
),
],
);

Step 6: Create Screens for Navigation

Create the necessary screen files:

  1. bottom_navigation_bar.dart:
import 'package:flutter/material.dart';

class BottomNavigationbar extends StatelessWidget {
final Widget child;

BottomNavigationbar({required this.child});

@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.favorite), label: 'Favorite'),
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'Books'),
],
),
body: child,
);
}
}
  1. home_screen.dart:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'home_controller.dart';

class HomeScreen extends StatelessWidget {
final HomeController controller = Get.put(HomeController());

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Obx(() {
return ListView.builder(
itemCount: controller.homeList.length,
itemBuilder: (context, index) {
final home = controller.homeList[index];
return ListTile(
title: Text(home.name),
subtitle: Text(home.description),
onTap: () {
context.go('/detail-homepage', extra: controller.homeList);
},
);
},
);
}),
);
}
}
  1. detail_home_page.dart:
import 'package:flutter/material.dart';
import 'home_model.dart';

class DetailHomePage extends StatelessWidget {
final List<HomeModel> listHome;

DetailHomePage({required this.listHome});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Home Page')),
body: ListView.builder(
itemCount: listHome.length,
itemBuilder: (context, index) {
final home = listHome[index];
return ListTile(
title: Text(home.name),
subtitle: Text(home.description),
);
},
),
);
}
}
  1. favorite_screen.dart:
import 'package:flutter/material.dart';

class FavoriteScreen extends StatelessWidget {
const FavoriteScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Favorite')),
body: Center(child: Text('Favorite Screen')),
);
}
}
  1. detail_favorite_screen.dart:
import 'package:flutter/material.dart';

class DetailFavoriteScreen extends StatelessWidget {
const DetailFavoriteScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Favorite')),
body: Center(child: Text('Detail Favorite Screen')),
);
}
}
  1. order_screen.dart:
import 'package:flutter/material.dart';

class OrderScreen extends StatelessWidget {
const OrderScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Order')),
body: Center(child: Text('Order Screen')),
);
}
}
  1. detail_order.dart:
import 'package:flutter/material.dart';

class DetailOrder extends StatelessWidget {
final int? id;

DetailOrder({this.id});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Order')),
body: Center(child: Text('Order ID: $id')),
);
}
}
  1. book_screen.dart:
import 'package:flutter/material.dart';

class BookScreen extends StatelessWidget {
const BookScreen({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Books')),
body: Center(child: Text('Book Screen')),
);
}
}
  1. detail_book.dart:
import 'package:flutter/material.dart';

class DetailBook extends StatelessWidget {
final int? id;

DetailBook({this.id});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Book')),
body: Center(child: Text('Book ID: $id')),
);
}
}

Step 7: Initialize the Router and Run the App

Update the main.dart file to initialize the router and run the app:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'app_router.dart';

void main() {
runApp(MyApp(router: router));
}

class MyApp extends StatelessWidget {
final GoRouter router;

MyApp({required this.router});

@override
Widget build(BuildContext context) {
return GetMaterialApp.router(
routerConfig: router,
);
}
}

Wait……... This is not the end 😵😵

To add route guards, custom transitions, and animations to your Flutter app using GoRouter and GetX, you’ll extend the existing setup with additional configurations and implementations. Here’s how you can achieve each of these:

Adding Route Guards

Route guards allow you to control access to routes based on certain conditions, such as authentication status. Let’s implement a simple route guard for authentication:

  1. Create an Authentication Service:

Create a service to manage authentication status. For simplicity, we’ll use a boolean value (isLoggedIn) to simulate authentication.

class AuthService extends GetxService {
bool isLoggedIn = false;

Future<void> login() async {
// Simulate a login process
await Future.delayed(Duration(seconds: 2));
isLoggedIn = true;
}

void logout() {
isLoggedIn = false;
}
}
  1. Update Main Setup:

Modify your main.dart to include the authentication service:

void main() {
final authService = AuthService();

runApp(MyApp(
router: router,
authService: authService,
));
}
  1. Implement Route Guard:

Update the ShellRoute in app_router.dart to use a route guard:

ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
// Access the authentication service
final authService = Get.find<AuthService>();

return Obx(() {
if (!authService.isLoggedIn) {
// Redirect to login if not logged in
return CircularProgressIndicator(); // Or navigate to login route
} else {
return BottomNavigationbar(child: child);
}
});
},
routes: [
// Existing route configurations...
],
),
  1. Here, we use Obx to observe changes in the authentication state (isLoggedIn). You can modify the guard logic as per your app's authentication flow.

Adding Custom Transitions and Animations

You can customize route transitions and animations using Flutter’s built-in animation controllers and the transition property of GoRoute. Let's add a fade transition as an example:

  1. Create a Fade Transition Widget:

Create a reusable widget for a fade transition:

class FadeRoute<T> extends MaterialPageRoute<T> {
FadeRoute({required WidgetBuilder builder, RouteSettings? settings})
: super(builder: builder, settings: settings);

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}
  1. Use Custom Transition in GoRoute:

Update a route in app_router.dart to use the custom fade transition:

GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/',
builder: (context, state) {
return const HomeScreen();
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
routes: <RouteBase>[
// Nested routes
],
),

Replace FadeRoute with your custom transition widget (FadeRoute)

Putting It All Together

Here’s how your updated app_router.dart might look with route guards and custom transitions:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:get/get.dart';
import 'home_model.dart';
import 'bottom_navigation_bar.dart';
import 'home_screen.dart';
import 'detail_home_page.dart';
import 'favorite_screen.dart';
import 'detail_favorite_screen.dart';
import 'order_screen.dart';
import 'detail_order.dart';
import 'book_screen.dart';
import 'detail_book.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> _shellNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'shell');

final router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/',
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
// Access the authentication service
final authService = Get.find<AuthService>();

return Obx(() {
if (!authService.isLoggedIn) {
// Redirect to login if not logged in
return CircularProgressIndicator(); // Or navigate to login route
} else {
return BottomNavigationbar(child: child);
}
});
},
routes: [
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/',
builder: (context, state) {
return const HomeScreen();
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
routes: <RouteBase>[
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: 'detail-homepage',
name: 'DetailHomePage',
builder: (BuildContext context, GoRouterState state) {
final list = state.extra as List<HomeModel>;
return DetailHomePage(listHome: list);
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
],
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/favorite',
name: 'Favorite',
builder: (BuildContext context, GoRouterState state) {
return const FavoriteScreen();
},
routes: [
GoRoute(
path: 'detail-fav',
name: 'DetailFav',
builder: (BuildContext context, GoRouterState state) {
return const DetailFavoriteScreen();
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
],
),
GoRoute(
parentNavigatorKey: _shellNavigatorKey,
path: '/order',
builder: (BuildContext context, GoRouterState state) {
return const OrderScreen();
},
routes: [
GoRoute(
path: 'detail-order',
name: 'DetailOrder',
builder: (BuildContext context, GoRouterState state) {
return DetailOrder(
id: int.tryParse(state.queryParams['id'] ?? ''),
);
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
],
),
],
),
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: '/books',
builder: (BuildContext context, GoRouterState state) {
return const BookScreen();
},
routes: [
GoRoute(
parentNavigatorKey: _rootNavigatorKey,
path: ':id',
builder: (BuildContext context, GoRouterState state) {
return DetailBook(
id: int.tryParse(state.params['id'] ?? ''),
);
},
transition: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
),
],
),
],
);

Summary

You’ve now enhanced your Flutter application with route guards for authentication, and custom transitions and animations using GoRouter and GetX. Route guards ensure secure navigation based on authentication status, while custom transitions provide smooth visual effects between screens. Adjust the implementations further based on your app’s specific requirements and styling preferences.

I Have added a sample app on my github.
Feel free to check.

Hope you enjoyed this article!

Clap 👏 If this article helps you.

See you all again soon!

--

--

EM Diya
EM Diya

Written by EM Diya

Flutter Developer | Happy about sharing my knowledge | Make learning Flutter and Software Development more fun and easier 💻🖱️

No responses yet