Flutter Lesson 34 of 83 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.