flutter-navegacion-paso-datos

Passing Data Between Screens in Flutter

  • 4 min

The passing of data is sending information from one screen to another during navigation.

Moving from one screen to another is great, but an App that doesn’t share information is a deaf App.

We need to pass data: tell the “Detail” screen which product the user tapped. And we also need to receive responses: have the “Date Selection” screen tell the previous screen which day the user chose.

Today we are going to see this bidirectional flow: sending arguments and receiving results.

Sending Data Forward (A -> B)

The simplest and most robust way to pass data to a new screen is by using the Constructor of the destination Widget.

If we treat screens as what they are (Dart Classes), passing data is as simple as instantiating that class with parameters.

Prepare the Destination Screen

First, we define what data the screen needs to work. We use final properties and request them in the constructor.

class DetailScreen extends StatelessWidget {
  // 1. Define the data we expect to receive
  final String productName;
  final double price;

  // 2. Require it in the constructor
  const DetailScreen({
    super.key, 
    required this.productName,
    required this.price
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(productName)),
      body: Center(child: Text("Costs \$$price")),
    );
  }
}
Copied!

Send the Data from the Origin

When performing Navigator.push, we simply pass the data when creating the widget.

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(
      productName: "Flutter Course", // Pass the data
      price: 99.99,
    ),
  ),
);
Copied!

It’s that simple! No weird tricks or global variables.

Receiving Data Back (B -> A)

Here’s where things get interesting. How does screen A know that screen B has finished and what the user decided?

The key is remembering that Navigator.push is an Asynchronous function that returns a Future.

Wait for the Result with await

In the origin screen, we must use await to pause the function’s execution until the user returns.

Future<void> _selectCity() async {
  // We wait for the screen to close and save what it returns
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => CitySelectionScreen()),
  );

  // When execution reaches here, screen B has already been closed
  if (result != null) {
    print("The user chose: $result");
  }
}
Copied!

Return the Data with pop

In the destination screen (Selection), when we want to close and send the data, we pass it as the second argument of the pop method.

// Inside CitySelectionScreen
ListTile(
  title: Text("Madrid"),
  onTap: () {
    // Close and send "Madrid" back
    Navigator.pop(context, "Madrid");
  },
);
Copied!

The Danger of the “Back” Button

There is a critical detail: The user might not select anything and simply press the “Back” arrow in the AppBar or the physical Android button.

In that case, the route closes without a result and the Future returns null.

Therefore, we should always check if the result is null before using it:

if (result != null) {
  // Do something with the data
} else {
  // The user cancelled the operation
}
Copied!

Complete Example: Emoji Selector

Let’s do a quick example. A main screen with a “Choose your mood” button, which opens a list of emojis. When one is tapped, we go back and update the text.

// --- SCREEN 1: HOME ---
class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  String _selectedEmoji = "❓";

  Future<void> _navigateAndWait() async {
    // 1. Go and WAIT (await)
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => SelectionScreen()),
    );

    // 2. Verify if it brought something (if they didn't return with "Back")
    if (!mounted) return;

    if (result != null) {
      setState(() {
        _selectedEmoji = result;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Passing Data")),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(_selectedEmoji, style: TextStyle(fontSize: 50)),
            ElevatedButton(
              onPressed: _navigateAndWait,
              child: Text("Select Mood"),
            )
          ],
        ),
      ),
    );
  }
}

// --- SCREEN 2: SELECTION ---
class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Choose one")),
      body: ListView(
        children: [
          ListTile(
            title: Text("Happy 😀"),
            onTap: () => Navigator.pop(context, "😀"), // Returns emoji
          ),
          ListTile(
            title: Text("Sad 😢"),
            onTap: () => Navigator.pop(context, "😢"),
          ),
          ListTile(
            title: Text("Coding 👨‍💻"),
            onTap: () => Navigator.pop(context, "👨‍💻"),
          ),
        ],
      ),
    );
  }
}
Copied!