Flutter Tutorial

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?

PassWhen
The whole objectYou already have it, and the details screen shows the same data
Only an idThe 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.

Practise in the playground Updated