Skip to content

Passing Data Between Screens

In React Router you pass data forward through URL params, route state (navigate('/detail', { state: item })), or query strings. You pass data back by lifting state up and passing a callback as a prop, or by using a global store. Flutter has a more explicit mechanism for both directions: pass data forward via widget constructor arguments (the same way you pass props), and pass data back by await-ing the Navigator.push future, which resolves to whatever value the next screen passes to Navigator.pop.

React
// React — pass forward via route state
// navigate('/detail', { state: { product } })
function ProductDetail() {
const { state } = useLocation();
const { product } = state;
return <h1>{product.name}</h1>;
}
// Pass back via callback prop
function EditScreen({ onSave }) {
return (
<button onClick={() => onSave({ name: 'Updated' })}>
Save
</button>
);
}
Flutter
// Flutter — pass forward via constructor
class ProductDetail extends StatelessWidget {
final Product product;
const ProductDetail({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Text(product.name);
}
}
// Push with data:
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ProductDetail(product: item),
),
);
// Pass back via pop return value:
final result = await Navigator.push<Map<String, String>>(
context,
MaterialPageRoute(builder: (_) => const EditScreen()),
);
// result == { 'name': 'Updated' }
// In EditScreen:
Navigator.pop(context, {'name': 'Updated'});
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: ContactListScreen());
  }
}

class Contact {
  final String name;
  final String email;
  const Contact({required this.name, required this.email});
}

const contacts = [
  Contact(name: 'Alice', email: '[email protected]'),
  Contact(name: 'Bob',   email: '[email protected]'),
  Contact(name: 'Carol', email: '[email protected]'),
];

class ContactListScreen extends StatelessWidget {
  const ContactListScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Contacts')),
      body: ListView.builder(
        itemCount: contacts.length,
        itemBuilder: (context, index) {
          final contact = contacts[index];
          return ListTile(
            title: Text(contact.name),
            subtitle: Text(contact.email),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => ContactDetailScreen(contact: contact),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class ContactDetailScreen extends StatelessWidget {
  final Contact contact;
  const ContactDetailScreen({super.key, required this.contact});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(contact.name)),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Name: ${contact.name}',
                style: const TextStyle(fontSize: 18)),
            const SizedBox(height: 8),
            Text('Email: ${contact.email}',
                style: const TextStyle(fontSize: 18)),
          ],
        ),
      ),
    );
  }
}
What is the recommended way to pass data forward to a new screen in Flutter?
How does a screen return data back to its caller in Flutter?
What does await Navigator.push<bool>(...) return?
What advantage does passing data via constructors have over React Router route state?