Tired of messy navigation code in Flutter? Say goodbye to nested
Navigator.push()and say hello to GoRouter + ShellRoute — your new routing superpowers 🧼
💡 The Problem
Managing navigation in Flutter used to feel like this:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SomePage()),
Sure, it works. But once you add:
- Bottom navigation tabs 📱
- Route guards 🔐
- Deep linking 🌐
- Nested stacks 🪜
…your code quickly becomes spaghetti 🍝
🌟 The GoRouter Solution
GoRouter is an official Flutter routing package that makes navigation:
✅ Declarative ✅ Readable ✅ Scalable ✅ Testable
And when you add ShellRoute to the mix… 🔥
You unlock shared UI between routes like bottom nav bars, side drawers, and more.
🧼 Benefits at a Glance

📦 Let's Build It: Bottom Nav + Independent Stacks
We'll create an app with 3 tabs:
- 🏠 Home
- 🔍 Search
- 👤 Profile
Each tab has its own navigation stack — just like a real app!
1️⃣ Set Up GoRouter
dependencies:
flutter:
sdk: flutter
go_router: ^13.0.2️⃣ Create Router with ShellRoute
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();
final GoRouter router = GoRouter(
navigatorKey: _rootNavigatorKey,
initialLocation: '/home',
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) => ScaffoldWithNavBar(child: child),
routes: [
GoRoute(
path: '/home',
pageBuilder: (_, __) => NoTransitionPage(child: HomeScreen()),
),
GoRoute(
path: '/search',
pageBuilder: (_, __) => NoTransitionPage(child: SearchScreen()),
),
GoRoute(
path: '/profile',
pageBuilder: (_, __) => NoTransitionPage(child: ProfileScreen()),
),
],
),
],
);3️⃣ Build Shared UI: Bottom Nav
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({required this.child});
static final tabs = [
{'label': 'Home', 'icon': Icons.home, 'location': '/home'},
{'label': 'Search', 'icon': Icons.search, 'location': '/search'},
{'label': 'Profile', 'icon': Icons.person, 'location': '/profile'},
];
int _currentIndex(BuildContext context) {
final location = GoRouter.of(context).location;
return tabs.indexWhere((tab) => location.startsWith(tab['location']!));
}
@override
Widget build(BuildContext context) {
final currentIndex = _currentIndex(context);
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
context.go(tabs[index]['location']!);
},
items: [
for (final tab in tabs)
BottomNavigationBarItem(
icon: Icon(tab['icon'] as IconData),
label: tab['label'] as String,
)
],
),
);
}
}4️⃣ Screens (Simple for Demo)
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Center(child: Text('🏠 Home'));
}
class SearchScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Center(child: Text('🔍 Search'));
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Center(child: Text('👤 Profile'));
}5️⃣ Entry Point
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}🔐 Bonus: Add Auth Guards
Want to protect routes like /profile?
GoRoute(
path: '/profile',
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
return isLoggedIn ? null : '/login';
},
pageBuilder: (_, __) => NoTransitionPage(child: ProfileScreen()),
),✅ Why This is a Game-Changer
- 🧼 No more push() chaos
- 💡 One source of truth for routes
- 🧭 Perfect for tabbed apps with nested navigation
- 🛡️ Secure with built-in guards
- 🌐 Web-ready with deep linking
🔚 Summary
Still managing routes manually?
Still struggling with nested tabs and login redirects?
It's time to upgrade.
🔗 GoRouter + ShellRoute = Next‑Level Flutter Navigation
Once you try it, you'll never go back.