Beginner · Lesson 6 · 10 min read
Navigation Between Screens
Push and pop screens, pass data both ways, and set up named routes — plus when you should graduate to a router package.
Updated July 24, 2026
What you will learn
- Push and pop routes with Navigator
- Pass data to a screen and return a result from it
- Define named routes and handle unknown paths
- Know when declarative routing is worth adopting
Flutter models navigation as a stack of routes. Pushing puts a new screen on top; popping removes it and reveals what was underneath. The back button and the iOS swipe gesture both just pop.
Push and pop
// Go to a new screen
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const DetailScreen()),
);
// Come back
Navigator.of(context).pop();
// Replace the current screen (no back button to it)
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
// Clear the whole stack — typical after login or logout
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const HomeScreen()),
(route) => false,
);MaterialPageRoute gives you the platform-appropriate transition automatically: a slide from the right on iOS, a fade-and-lift on Android.
Passing data forward
The simplest approach is a constructor argument. It is type-safe and the compiler catches mistakes.
class LessonDetailScreen extends StatelessWidget {
const LessonDetailScreen({super.key, required this.lesson});
final Lesson lesson;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(lesson.title)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text(lesson.summary),
),
);
}
}
// At the call site
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LessonDetailScreen(lesson: lesson),
),
),Returning data back
push returns a Future that completes when the pushed route pops. Whatever you pass to pop becomes that future's value.
Future<void> _pickFilter(BuildContext context) async {
final selected = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (_) => const FilterScreen()),
);
if (!context.mounted) return; // route may have been disposed
if (selected == null) return; // user pressed back without choosing
setState(() => _filter = selected);
}
// Inside FilterScreen
ListTile(
title: const Text('Beginner'),
onTap: () => Navigator.of(context).pop('beginner'),
)Named routes
For small-to-medium apps, named routes keep navigation code out of your widgets and give you a single place to see every screen.
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/settings': (context) => const SettingsScreen(),
'/about': (context) => const AboutScreen(),
},
// Handle routes not in the map — deep links, typos
onUnknownRoute: (settings) => MaterialPageRoute(
builder: (_) => const NotFoundScreen(),
),
)
// Navigate by name
Navigator.of(context).pushNamed('/settings');
// With an argument
Navigator.of(context).pushNamed('/lesson', arguments: lessonId);
// Read it in the target screen
final lessonId = ModalRoute.of(context)!.settings.arguments as String;That cast on the last line is the weakness of named routes: arguments are Object?, so type errors surface at runtime instead of compile time. For anything beyond a handful of screens, this cost adds up.
Dialogs, sheets and snackbars
// Confirmation dialog that returns a bool
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete lesson?'),
content: const Text('This cannot be undone.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
),
);
// Bottom sheet
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (context) => const FilterSheet(),
);
// Snackbar — note it uses ScaffoldMessenger, not Navigator
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Saved')),
);Dialogs and sheets are routes too — that is why they take a context, return a future, and are dismissed with pop.
When to move to a router package
Navigator and named routes cover most apps. Adopt a declarative router such as go_router when you need:
- Deep links and web URLs that map cleanly to screens, including path parameters
- Redirects — bouncing signed-out users to login from any route
- Nested navigation — a bottom navigation bar where each tab keeps its own stack
- Type-safe routes generated from your route definitions
Key takeaways
- Navigation is a stack:
pushadds,popremoves, andpushreturns a future carrying the popped value. - Constructor arguments are the type-safe way to pass data; named-route arguments require an unsafe cast.
- Always check
context.mountedbefore using a context after an await. - Dialogs, sheets and the snackbar follow the same route-and-future pattern (snackbars use
ScaffoldMessenger).
Practice
Two-screen notes app
Build a list of notes with a floating action button that pushes an editor screen. When the editor pops, return the new note and insert it into the list. Add a tap-to-edit flow that pre-fills the editor and returns the updated note, plus a delete confirmation dialog.
Show hints
- Type your push:
push<Note>(...)so the returned future isFuture<Note?>. - A null result means the user backed out — leave the list unchanged.