Navigation is only half the story — most screens need to carry data with them. You open a product detail screen with a product, or a picker that hands a selection back. Flutter gives you two directions: pass data forward through the destination's constructor, and return a result backward by awaiting the push and popping with a value. This guide covers both directions, named-route arguments, and the null-result trap, with paste-ready code.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowEvery snippet below is paste-ready against current stable Flutter. This builds directly on the navigation basics — push and pop — so reach for that first if the stack model is new to you.
We'll pass data forward via the constructor, return a value with an awaited push, send arguments through a named route, and handle the case where nothing comes back.
If you'd rather watch a list-to-detail flow with a result handed back, the channel builds these patterns inside real apps.
Pass data forward: the constructor
The cleanest way to send data to a screen is through its constructor. The destination declares what it needs as a required field, and you supply it when you build the route — the compiler checks the type for you.
class DetailsScreen extends StatelessWidget {
const DetailsScreen({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Center(child: Text(product.description)),
);
}
}
// Navigate, passing the data in:
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailsScreen(product: tappedProduct)),
);
This is the approach to prefer. Because the data flows through a typed constructor, there's no casting and no chance of a missing-argument runtime error — the screen simply can't be built without its data.
Return a value: await the push
Navigator.push returns a Future that completes when the pushed screen pops. Await it, and on the second screen pass the result to Navigator.pop — the awaited push then resolves to that value.
// First screen — open a picker and await the choice:
Future<void> _pickColour() async {
final selected = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (_) => const ColourPickerScreen()),
);
if (selected != null) {
setState(() => _colour = selected);
}
}
// Second screen — return the chosen value:
ElevatedButton(
onPressed: () => Navigator.pop(context, 'Indigo'),
child: const Text('Choose Indigo'),
)
This round-trip is how selection screens, pickers, and confirmation flows work. The first screen pauses at the await, the second hands a value back through pop, and execution resumes with the result in hand — the same async/await flow you'll use throughout Flutter.

Wire screens together the right way
The Complete Flutter Guide covers navigation, data flow, and state management end to end.
Enrol nowArguments with named routes
When you navigate by name, you can't pass a constructor argument directly. Instead, pass an arguments object and read it on the destination via ModalRoute — or, more robustly, parse it inside onGenerateRoute.
// Send:
Navigator.pushNamed(context, '/details', arguments: tappedProduct);
// Receive on the destination screen:
final product = ModalRoute.of(context)!.settings.arguments as Product;
Route arguments are loosely typed, so the cast can fail at runtime if you send the wrong shape. When you build the route yourself, constructor arguments are safer; reserve route arguments for when you must go through the named-route table — for example with deep links, where go_router gives you typed path and query parameters instead.
Handle the empty result
The user might not make a choice at all — they can dismiss the second screen with the back button or the system gesture, which pops with no value. The awaited push then resolves to null, so always guard for it before acting on the result, exactly as the picker example above does with its if (selected != null) check.
Common mistakes
- Using route arguments when a constructor would do. Constructor data is type-checked; route arguments cast at runtime.
- Not awaiting the push. Without
awaityou never receive the popped value. - Assuming a result is always returned. A back-button dismissal pops with
null; handle it. - Calling setState after await without a mounted check. Guard with
if (!mounted) return;after the await. - Casting route arguments without a null check. A wrong or missing argument throws; validate before casting.
Frequently asked questions
How do I pass data to a new screen in Flutter?
Through the destination screen's constructor — declare a required field and pass it when you build the MaterialPageRoute. It's the type-safe option.
How do I return a value from a screen in Flutter?
Await Navigator.push and pop the second screen with Navigator.pop(context, value); the awaited push resolves to that value.
How do I pass arguments to a named route in Flutter?
Pass arguments: to pushNamed and read them via ModalRoute.of(context)!.settings.arguments or in onGenerateRoute.
Why is the returned value null after Navigator.pop in Flutter?
The screen was dismissed with the back button or gesture, which pops without a value. Always handle the null case after awaiting.
Further reads
Keep going with the tutorials that pair with this guide:
- Flutter Development Guide 2026 — the full Flutter hub.
- Flutter Navigation Basics — push, pop, and named routes.
- Flutter BottomNavigationBar — multi-tab navigation.
- Flutter Date and Time Pickers — the same await-a-result pattern with dialogs.
- go_router in Flutter — typed parameters and deep linking.
Sources: Flutter documentation — Navigator.push, Navigator.pop, RouteSettings, ModalRoute, and the "Send data to a new screen" and "Return data from a screen" cookbook recipes (docs.flutter.dev). Verified against current stable Flutter.