1. Navigator.push/pop & Passing Data
Navigator maintains a stack of routes, same conceptual shape as a
browser's history — push adds a screen on top, pop removes
it and returns to whatever was underneath.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(productId: 42),
),
);
// inside ProductDetailScreen — pop and hand a value back
Navigator.pop(context, 'added-to-cart');
// back on the original screen — awaiting the result
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ProductDetailScreen()),
);
if (result == 'added-to-cart') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Added to cart!')),
);
}
Because push returns a Future, await-ing it
is the idiomatic way to react to whatever the pushed screen eventually pops back —
no manual callback wiring required.
2. Named Routes & a Route Table
Constructing a screen widget directly at every call site works for a small app, but scatters route names and screen construction logic everywhere. A centralized route table fixes that.
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const ProductListScreen(),
'/cart': (context) => const CartScreen(),
'/settings': (context) => const SettingsScreen(),
},
)
Navigator.pushNamed(context, '/cart');
For a route that needs an argument (like productId), a name-only table
isn't quite enough on its own — onGenerateRoute handles that case, but
it's also exactly the gap next week's go_router fills more cleanly,
with type-safe params and URL-style paths built in from the start.
4. Hands-on Exercise
Build a 3-screen app with navigation and a result round-trip
Combine push/pop with a result, named routes, and a bottom nav bar in one small app.
Requirements:
- A route table in
main.dartwith named routes for at least 3 screens. - A product list screen where tapping an item uses
Navigator.pushto open a detail screen, passing the product's ID as a constructor argument. - An "Add to Cart" button on the detail screen that pops with a result; the list screen awaits the push and shows a
SnackBarwhen the result indicates success. - A
NavigationBarswitching between at least 3 top-level tabs, with each tab's scroll position preserved when switching away and back. - Confirm the Android hardware back button (or an equivalent gesture) correctly pops the detail screen rather than exiting the app or misbehaving.
If a tab's scroll position resets every time you switch away and back, check that the widgets in your _screens list are being recreated on every build rather than reused — keeping them as static const (or otherwise stable instances) is what lets Flutter preserve each tab's internal state across selections.
5. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does await-ing Navigator.push work correctly, given that the pushed screen might stay open for an arbitrary amount of time?
Why does await-ing Navigator.push work correctly, given that the pushed screen might stay open for an arbitrary amount of time?
Navigator.push returns a Future that doesn't complete until the pushed route is popped — await simply suspends the calling code until that eventually happens, how ever long it takes, and then resumes with whatever value (or null) was passed to pop.
Q2
What real problem does a centralized route table solve compared to constructing screen widgets directly at every navigation call site?
What real problem does a centralized route table solve compared to constructing screen widgets directly at every navigation call site?
It gives every route one canonical name and one place where its construction logic lives — call sites just reference a route name rather than each independently knowing how to build the target screen, which avoids the duplication and drift that comes from repeating "here's how you build the cart screen" wherever it might be navigated to.
Q3
Why is switching between tabs in a bottom navigation bar not the same operation as Navigator.push?
Why is switching between tabs in a bottom navigation bar not the same operation as Navigator.push?
Switching tabs just changes which already-built widget in a list is currently displayed — it doesn't add anything to the navigation stack. Navigator.push genuinely pushes a new route onto the stack, which is why the two produce different back-button behavior: popping a pushed screen returns to what was underneath, while switching tabs has no "underneath" to return to in the stack sense.
Q4
Why does the exercise call out testing the Android back button specifically, rather than assuming push/pop alone guarantees correct behavior?
Why does the exercise call out testing the Android back button specifically, rather than assuming push/pop alone guarantees correct behavior?
The hardware back button is wired to the same navigation stack Navigator.push/pop manage, so it should pop the topmost pushed route by default — but it's a real, separate interaction path from any in-app back button you might build, and a bug in how a screen is pushed (or a custom WillPopScope/PopScope override) can make it behave incorrectly even when an in-app back button works fine, which is exactly why it needs its own explicit check.