How do you handle navigation in Flutter?
3 min read
Widgets & UI
| Imperative (Navigator 1.0) | Declarative (go_router, 2.0) | |
|---|---|---|
| Mental model | "Push this, pop that" | "Here's the URL → here's the screen" |
| Best for | Simple apps, modal dialogs, picker screens | Deep links, web, nested tabs, complex flows |
| Web URLs | Manual handling | First-class |
| Type-safe args | Awkward | Easy (with go_router extras) |
| Boilerplate | Low for small apps, grows with complexity | Small upfront cost, scales well |
Imperative — the everyday API
// Push a new screen
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => DetailScreen(id: '123')),
);
// Pop the current screen
Navigator.of(context).pop();
// Replace current (no back) — e.g. login → home
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomeScreen()),
);
// Clear the stack and push (e.g. logout → login)
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false,
);
// Pop until root
Navigator.of(context).popUntil((route) => route.isFirst);
Pass data and get a result back
// A → push and await
final picked = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (_) => const PickerScreen()),
);
if (picked != null) print(picked);
// B → pop with a result
Navigator.of(context).pop('selected-item');
Declarative — go_router (the modern default)
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (ctx, st) => const HomeScreen(),
routes: [
GoRoute(
path: 'user/:id',
builder: (ctx, st) => UserScreen(userId: st.pathParameters['id']!),
),
],
),
],
);
// In the app
MaterialApp.router(routerConfig: router);
// Navigate
context.go('/user/123'); // replace stack
context.push('/user/123'); // push on top
context.pop();
URL is the source of truth — same routing logic works on mobile and web, and deep links "just work."
When to reach for which
| Situation | Use |
|---|---|
| 3–5 screens, no deep links | Imperative Navigator |
| Modal dialogs / pickers | Imperative (push + await) |
| Web app or deep-link-driven flows | go_router |
| Nested tabs, each with their own back stack | go_router (ShellRoute) |
| Auth gating (redirect if not logged in) | go_router's redirect |
| Hand-rolled state-driven routing | Navigator 2.0 Router API (rarely needed directly) |
Common mistakes to avoid
// ❌ Using context after an async gap without mounted check
await api.save(data);
Navigator.of(context).pop(); // 💥 if widget gone
// ✅
if (!context.mounted) return;
Navigator.of(context).pop();
// ❌ Passing data via global state / singletons for short-lived screens
// ✅ Pass via constructor; for go_router, use path params + extras
// ❌ pushReplacement on a flow that needs back navigation
// ✅ push if the user should be able to return; pushReplacement only when "no going back"
// ❌ Wiring named routes by hand without arguments parsing helpers
// ✅ Either use go_router, or wrap ModalRoute.of(context)!.settings.arguments in a typed helper
// ❌ Stacking modal bottom sheets on top of dialogs etc. without thinking
// Each opens a new route — track UX implications, not just code structure
Interview follow-ups
-
What's the difference between
push,pushReplacement, andpushAndRemoveUntil?pushadds on top of the stack.pushReplacementswaps the current route for the new one (no back).pushAndRemoveUntilclears routes from the top until a predicate matches, then pushes — used for "log out and go to login." -
How do you return a value from a pushed screen?
await Navigator.push<T>(...)returns whatever the pushed route pops with:Navigator.pop(context, value). The await resumes when that route is popped —nullif the user dismissed without choosing. -
Why use
go_routerover plain Navigator? URL-based source of truth, automatic deep link handling, type-safe route definitions,redirectfor auth,ShellRoutefor nested layouts, and clean separation between "where am I" and "how do I get there." -
What is Navigator 2.0 and is
go_router"Navigator 2.0"? Navigator 2.0 is the low-level declarativeRouterAPI Flutter ships. It's powerful but verbose.go_routeris the recommended package built on top of it — same model, far less boilerplate.
How helpful was this content?
Please sign in to rate this article.