Flutter Intermediate Lesson 35 of 135 3 min read
Passing Data Between Screens in Flutter
Send data to a new screen through its constructor and return a result with Navigator.pop in Flutter, with typed results and examples.
On this page
Data flows in two directions between screens: forward when you open one, and back when it closes.
Forward: through the constructor
A page is a widget, and widgets take constructor arguments.
import 'package:flutter/material.dart';
class Product {
const Product(this.name, this.price);
final String name;
final int price;
}
const products = [
Product('Notebook', 120),
Product('Backpack', 1800),
Product('Water bottle', 450),
];
void main() => runApp(const MaterialApp(home: ProductListPage()));
class ProductListPage extends StatelessWidget {
const ProductListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: ListView(
children: [
for (final product in products)
ListTile(
title: Text(product.name),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => ProductPage(product: product),
),
),
),
],
),
);
}
}
class ProductPage extends StatelessWidget {
const ProductPage({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('Rs. ${product.price}', style: const TextStyle(fontSize: 32))),
);
}
}
This is type safe. Forget the argument, or pass the wrong type, and the code does not compile.
Back: a result from pop
Navigator.push returns a Future that completes when the pushed screen closes. The value you give pop becomes the result.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: ProfilePage()));
class ProfilePage extends StatefulWidget {
const ProfilePage({super.key});
@override
State<ProfilePage> createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> {
String _city = 'Not set';
Future<void> _chooseCity() async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const CityPickerPage()),
);
if (result == null) return; // the user went back without choosing
setState(() => _city = result);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: ListTile(
title: const Text('City'),
subtitle: Text(_city),
trailing: const Icon(Icons.edit),
onTap: _chooseCity,
),
);
}
}
class CityPickerPage extends StatelessWidget {
const CityPickerPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Choose a city')),
body: ListView(
children: [
for (final city in ['Kathmandu', 'Pokhara', 'Dharan'])
ListTile(
title: Text(city),
onTap: () => Navigator.pop(context, city), // send it back
),
],
),
);
}
}
Two things to remember:
- State the result type:
Navigator.push<String>(...). - The result is nullable. The user can always leave with the back button, which gives
null.
Returning more than one value
Return a record or an object.
// In the editing screen
Navigator.pop(context, (name: _name.text, age: int.parse(_age.text)));
// In the caller
final result = await Navigator.push<({String name, int age})>(
context,
MaterialPageRoute(builder: (_) => const EditPersonPage()),
);
if (result != null) {
debugPrint('${result.name}, ${result.age}');
}
Refreshing a list after an edit
A common pattern: open an edit screen, then reload when it reports that something changed.
Future<void> _openEditor(Note note) async {
final changed = await Navigator.push<bool>(
context,
MaterialPageRoute(builder: (_) => EditNotePage(note: note)),
);
if (changed == true) {
await _reloadNotes();
}
}
// In EditNotePage, after saving:
Navigator.pop(context, true);
Pass an id or the whole object?
| Pass | When |
|---|---|
| The whole object | You already have it, and the details screen shows the same data |
| Only an id | The details screen loads fresh data, or the screen can be opened from a link |
With URL-based routing you can only put simple values in a URL, so screens usually receive an id and load the rest themselves.
When constructor passing gets painful
If you find yourself passing the same object through three or four screens that do not use it, only so that a distant screen can, that data should live in shared state above the navigator. See state management.
Try it yourself
Build a contacts app with two screens. The list screen has an “Add” button that opens a form. The form returns a record with a name and phone number, and the list screen adds it and shows a SnackBar.