Flutter Navigation: Push, Pop and Pass Data Between Screens

Moving between screens and carrying data along with you is one of the first things every Flutter app needs. This covers the three common situations: pushing a new screen, passing data to it, and getting a result back.

Pushing a New Screen

The basic building block is Navigator.push, which adds a new screen on top of the current one:

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => const DetailScreen()),
);

Going back to the previous screen is Navigator.pop(context), usually called automatically by the back button/gesture, or explicitly from a button in your own UI.

Passing Data Forward

Pass data to the new screen the same way you’d pass any constructor parameter — there’s no special API for it:

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailScreen(product: selectedProduct),
  ),
);

class DetailScreen extends StatelessWidget {
  final Product product;
  const DetailScreen({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Text(product.name);
  }
}

Getting a Result Back

For screens that should return a value — a settings screen returning the chosen option, a form returning the entered data — await the result of Navigator.push, and pass a value into Navigator.pop on the way back:

final selectedColor = await Navigator.push<Color>(
  context,
  MaterialPageRoute(builder: (context) => const ColorPickerScreen()),
);

if (selectedColor != null) {
  setState(() => _backgroundColor = selectedColor);
}
// Inside ColorPickerScreen, when the user makes a choice:
Navigator.pop(context, Colors.blue);

The generic type on Navigator.push<Color> tells Dart what type to expect back, so selectedColor is typed correctly rather than being dynamic.

Named Routes as an Alternative

For apps with many screens, named routes registered in MaterialApp.routes avoid importing every screen’s class wherever you navigate:

MaterialApp(
  routes: {
    '/detail': (context) => const DetailScreen(),
  },
)

Navigator.pushNamed(context, '/detail', arguments: selectedProduct);

// On the receiving screen:
final product = ModalRoute.of(context)!.settings.arguments as Product;

The tradeoff is that argument passing loses compile-time type safety — the cast to Product can fail at runtime if the wrong type is passed. For a small-to-medium app, directly constructing the destination widget (as in the examples above) is usually simpler and safer; named routes pay off more as the number of screens grows.