Hey fellow Flutter dev!
Flutter's navigation can sometimes feel like a puzzle, especially as your app grows. You start with Navigator.push and pop, but soon you're dealing with deep links, authentication flows, and nested navigation that can quickly turn into a headache.
If you've ever found yourself wishing for a simpler, more declarative way to manage your app's routes, you're in the right place!
Let’s get started!
Why GoRouter?
Before GoRouter, Flutter navigation could be a real struggle. We used Navigator 1.0 (you know, push and pop), or sometimes got tangled up with Navigator 2.0 and Router widgets.
They're powerful, sure, but they could get super tricky when you needed to handle:
- Deep Linking: Getting to specific parts of your app straight from a link.
- Web Support: Making your Flutter web app act like a normal website with proper URLs.
- Authentication Flows: Guiding users through login and making sure they’re in the right place.
- Complex Navigation: Like when you have tabs or bottom navigation, and each one has its own history.
GoRouter swoops in to solve all these problems! It gives you a much cleaner and easier way to handle all your app’s navigation.
Getting Started: Installation and Basic Setup
First things first, let's get go_router into your project. Just open your pubspec.yaml file and pop this in:
dependencies: flutter: sdk: flutter go_router: ^14.0.0 # Use the latest stable version
Then, run flutter pub get in your terminal. Easy peasy!
Now, for the basic setup. Instead of MaterialApp, we'll use MaterialApp.router. It's pretty straightforward!
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() { runApp(const MyApp()); } // Define your routes final GoRouter _router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/details', builder: (context, state) => const DetailsScreen(), ), ], ); class MyApp extends StatelessWidget { const MyApp({super.key}); Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _router, title: 'GoRouter Example', theme: ThemeData( primarySwatch: Colors.blue, ), ); } } // Simple Placeholder Screens class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Home Screen')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Welcome to the Home Screen!'), ElevatedButton( onPressed: () { context.go('/details'); }, child: const Text('Go to Details'), ), ], ), ), ); } } class DetailsScreen extends StatelessWidget { const DetailsScreen({super.key}); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Details Screen')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('You are on the Details Screen!'), ElevatedButton( onPressed: () { context.pop(); }, child: const Text('Go Back'), ), ], ), ), ); } }
Navigating with GoRouter: go() vs. push()
GoRouter has two main ways to move around, and knowing the difference is super important!
-
context.go('/path')- Replaces your entire navigation history.
- Great for deep links or login screens.
-
context.push('/path')- Adds the new route on top of the stack.
- Use this for in-app flows like detail pages.
Example using push():
ElevatedButton( onPressed: () { context.push('/details'); }, child: const Text('Go to Details (Push)'), )
Leveraging Named Routes for Better Code
Typing out paths like '/details' everywhere can get messy. Named routes keep things clean:
final GoRouter _router = GoRouter( initialLocation: '/', routes: [ GoRoute( path: '/', name: 'home', builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/details', name: 'details', builder: (context, state) => const DetailsScreen(), ), ], );
ElevatedButton( onPressed: () { context.goNamed('details'); }, child: const Text('Go to Details (Named)'), )
Passing Data Between Routes
1. Path Parameters
GoRoute( path: '/product/:id', name: 'productDetail', builder: (context, state) { final productId = state.pathParameters['id']; return ProductDetailScreen(productId: productId!); }, ); // Navigation context.goNamed('productDetail', pathParameters: {'id': 'flutter-book-123'});
2. Query Parameters
GoRoute( path: '/products', name: 'productsList', builder: (context, state) { final category = state.queryParameters['category']; final sortBy = state.queryParameters['sort']; return ProductsListScreen(category: category, sortBy: sortBy); }, ); // Navigation context.goNamed( 'productsList', queryParameters: {'category': 'books', 'sort': 'asc'}, );
3. extra Object
class Product { final String id; final String name; final double price; Product(this.id, this.name, this.price); } GoRoute( path: '/product-extra', name: 'productExtraDetail', builder: (context, state) { final product = state.extra as Product?; return ProductDetailScreenWithExtra(product: product!); }, ); // Navigation final myProduct = Product('laptop-456', 'Super Laptop Pro', 1200.0); context.goNamed('productExtraDetail', extra: myProduct);
⚠️ Note:
extradoesn't persist across app restarts or web refreshes.
What's Next? GoRouter's Power Features
This post is just the beginning! GoRouter also supports:
- Redirection (
redirect): Useful for login protection. - Nested Navigation (
ShellRoute,StatefulShellRoute): Great for bottom nav bars. - Error Handling: Custom 404 pages and fallback routes.
Best Practices & Tips
- 📁 Keep Your Routes Together: Put all GoRoutes in a file like
app_router.dart. - 🔖 Always Use Named Routes: Easier to refactor.
- 🔁 Know When to Use
go()vs.push(). - 🌐 Use Friendly URLs: Especially if you're building for web.
Conclusion
GoRouter really simplifies navigation in Flutter. It's URL-friendly, declarative, and super flexible. Once you understand routes, go() vs. push(), and how to pass data you're ready to build amazing apps!
Happy coding, Khmercoders! 🚀