Flutter Navigation Basics: Navigator.push & Pop Tutorial | (Ch. 13)
Welcome to Chapter 13! In the previous chapter, we built the "skeleton" of a complex app. We learned how to add an AppBar at the top, a Drawer on the side, and a BottomNavigationBar at the footer.
But there was one major piece missing. When we tapped on "Profile" or "Settings," nothing happened. We were stuck on the home screen.
An app with only one screen isn't much of an app. To build real software, we need to know how to move users from Screen A to Screen B, and how to get them back safely.
This concept is called Navigation.
In Flutter, Navigation is built on a very simple, elegant metaphor: a Stack of Pancakes. In this chapter, we will master the two most fundamental commands in all of Flutter development: Navigator.push() and Navigator.pop().
📖 Chapter 13: Table of Contents
1. The Mental Model: The Navigation Stack
Before we write a single line of code, you must understand how Flutter manages screens. Flutter doesn't "replace" screens like a website changes URLs. Instead, it "stacks" them.
The Pancake Analogy
Imagine you have a plate.
-
Home Screen: When your app starts, Flutter puts one pancake on the plate. This is your
Homewidget. You are looking at the top pancake. - Push: When you navigate to the "Settings" screen, Flutter puts a *new* pancake on top of the Home pancake. Now you are looking at Settings. The Home screen is still there! It's just underneath, hidden.
- Push Again: If you go from Settings to "Account Details," Flutter puts a *third* pancake on top.
- Pop: When you hit the "Back" button, Flutter takes the top pancake (Account Details) and throws it in the trash. What do you see? The Settings pancake again!
This data structure is called a Stack (Last In, First Out). The Navigator widget is the chef that manages this stack.
Routes vs. Screens
In standard conversation, we say "Screen" or "Page." In Flutter terminology, these are called Routes.
A Route is an abstraction. It wraps your widget (like SettingsScreen) and gives it transition animations (like sliding in from the right on iOS or fading in on Android).
2. Going Forward: `Navigator.push()`
To add a new pancake to the stack, we use the push method.
The `MaterialPageRoute` Wrapper
You cannot just push a widget directly. You have to wrap it in a Route. The most common one is MaterialPageRoute.
MaterialPageRoute does two things:
- It holds the widget you want to show (the
builder). - It handles the platform-specific animation. On an iPhone, it will make the new screen slide in from the right. On Android, it might zoom or fade in. It handles this automatically.
The Syntax
Here is the standard code you will write thousands of times:
// Inside a button's onPressed:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
Let's break it down:
context: This tells the Navigator *where* in the widget tree we currently are, so it can find the nearest Navigator.MaterialPageRoute: The wrapper we discussed.builder: A function that creates the widget we want to go to (SecondScreen).
3. Going Back: `Navigator.pop()`
To remove the top pancake and go back to the previous one, we use pop.
The Physical Back Button
If you are using a Scaffold with an AppBar, Flutter automatically gives you a "Back" arrow button in the top-left corner whenever there is a screen underneath the current one.
On Android, the physical (or gesture) back button on the phone also triggers a pop automatically.
However, sometimes you want to trigger this manually (e.g., a "Cancel" or "Save" button).
The Syntax
// Inside a button's onPressed:
Navigator.pop(context);
That's it! It destroys the current screen widget (calling its dispose method if it's stateful) and reveals the screen underneath.
4. Passing Data TO a New Screen
Navigating is easy. But usually, you need to send data along with the user. For example, clicking a product in a list should open the "Details" screen *for that specific product*.
Constructor Injection
The best way to do this is simply by using the Constructor of the widget you are navigating to.
1. Define the parameter in the destination widget.
2. Pass the value when you call the constructor in push.
Example: Passing a "Todo" Item
Step 1: The Destination Screen
class DetailScreen extends StatelessWidget {
// 1. Declare the field
final String todoText;
// 2. Require it in the constructor
const DetailScreen({super.key, required this.todoText});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Task Details")),
body: Center(
child: Text(todoText), // 3. Use the data
),
);
}
}
Step 2: Pushing the Screen
final String myTask = "Buy Milk";
Navigator.push(
context,
MaterialPageRoute(
// Pass the data right here!
builder: (context) => DetailScreen(todoText: myTask),
),
);
5. Returning Data FROM a Screen
This is slightly more advanced, but very necessary. Imagine a "Selection Screen."
- Screen A asks: "Choose a generic color."
- User taps button to go to Screen B.
- Screen B shows a list: Red, Blue, Green.
- User taps "Blue."
- Screen B closes (pops), and Screen A now knows the user picked "Blue."
The `async` / `await` Pattern
In Dart, Navigator.push() actually returns a Future. This Future "completes" when the pushed screen is popped.
We can await the result of the push!
// In Screen A (The asker)
void _navigateAndGetSelection(BuildContext context) async {
// 1. Wait for the push to return
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// 2. Use the result (after Screen B closes)
print("User selected: $result");
}
Popping with Result Data
In Screen B, when we pop, we pass an optional second argument.
// In Screen B (The option list)
// When user taps "Blue":
Navigator.pop(context, "Blue");
6. Full Example: The Selection Screen
Let's build a mini-app to demonstrate this two-way data flow.
File: main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: FirstScreen()));
}
// --- SCREEN 1: THE ASKER ---
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});
@override
State<FirstScreen> createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
String _selection = "Nothing selected yet";
// The method to navigate
void _navigateAndDisplaySelection(BuildContext context) async {
// AWAIT the result from Screen 2
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// If the user backed out without selecting, result might be null
if (!mounted) return; // Safety check for async code
setState(() {
_selection = result ?? "User canceled selection";
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Returning Data Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_selection, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _navigateAndDisplaySelection(context),
child: const Text('Pick an Option'),
),
],
),
),
);
}
}
// --- SCREEN 2: THE OPTIONS ---
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Pick an option')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
// Return "Yes" to Screen 1
Navigator.pop(context, 'Yes!');
},
child: const Text('Yes!'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
// Return "No" to Screen 1
Navigator.pop(context, 'No.');
},
child: const Text('No.'),
),
],
),
),
);
}
}
7. Common Pitfalls
Navigation seems simple, but beginners often hit these walls:
-
The Black Screen of Death: If you navigate to a new widget that does not have a
Scaffold(orMaterialwidget) as its root, the background will be black and the text will be ugly red/yellow with double underlines.
Fix: Always wrap your new screen content in aScaffold. -
"Context" Issues:
Navigator.of(context)looks up the widget tree to find the Navigator. If you try to call this from a context where the Navigator doesn't exist yet (rare, but happens in `main()`), it will fail. -
Async "Mounted" Checks: When you use `await Navigator.push`, your widget might be destroyed (e.g., user rotates phone or app closes) while the user is on the second screen. When they return, calling `setState` on a destroyed widget throws an error.
Fix: Always addif (!mounted) return;after theawaitline.
❓ FAQ: Frequently Asked Questions
- Animations: The slide/fade transition users expect.
- Back Button: The automatic back arrow in the AppBar.
- Hardware Back Support: The Android back button works automatically.
push: Adds a pancake. You can go back. (e.g., List -> Details).pushReplacement: Swaps the current pancake with a new one. You cannot go back to the old one. This is used for things like Login screens. Once you log in and go to Home, hitting "Back" shouldn't take you back to the Login screen; it should exit the app.
Conclusion
You now hold the keys to multi-screen applications.
You understand that screens are just widgets stacked on top of each other. You know how to Push a new one, Pop the old one, Pass Data forward via constructors, and Return Data backward via `Future`s.
In the next chapter, we will look at a more organized way to handle navigation for larger apps using Named Routes and the GoRouter package, which is the modern standard for complex Flutter applications.

Comments
Post a Comment