Flutter Pass Data Between Screens: Arguments and Return Values

Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter pass data between screens guide, with constructor arguments going forward and a returned value coming back from pop.
Coding Liquids blog cover featuring Sagnik Bhattacharya for the Flutter pass data between screens guide.

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 course thumbnail

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 now

Every 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.

Follow me on Instagram@sagnikteaches

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.

Connect on LinkedInSagnik Bhattacharya

If you'd rather watch a list-to-detail flow with a result handed back, the channel builds these patterns inside real apps.

Subscribe on YouTube@codingliquids

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.

The Complete Flutter Guide course thumbnail

Wire screens together the right way

The Complete Flutter Guide covers navigation, data flow, and state management end to end.

Enrol now

Arguments 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 await you 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:

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.