🧭 Stack Based Navigation — Move Between Screens Like a Pro!
🧒 How Do Apps Move Between Screens? (The Stack of Cards Analogy!)
Right Now, Your App Shows ONE Screen — But Real Apps Have Many!
Your Rolodex app currently shows the contact list, but there's no way to tap on a contact group and see its details. In a real app, screens stack on top of each other like a deck of cards!
Think of your phone's app screens like a stack of playing cards 🃏:
- 📥 Push — Place a NEW card on top of the stack (go to a new screen)
- 📤 Pop — Remove the TOP card from the stack (go back to the previous screen)
- 👀 You can only see the top card — the screen on top of the stack
- 📱 Phone: Stack navigation — tap a group, push a new screen, swipe back
- 💻 Tablet/Desktop: Sidebar navigation — click a group, update the detail panel (no pushing!)
- ✅ Both patterns work from the same code — Flutter picks the right one!
Navigator.push() and Navigator.pop() — The Two Magic Words
Flutter has a built-in Navigator that manages the stack of screens. You only need to know two commands:
Navigator.push()
"Go to a new screen!" Puts a new screen on top of the stack. The old screen is still underneath, waiting.
Navigator.of(context).push(
CupertinoPageRoute(
builder: (context) => NewScreen(),
),
);Navigator.pop()
"Go back!" Removes the top screen, revealing the one underneath. Like pressing the back button.
Navigator.of(context).pop();Two Navigation Patterns, One Codebase
Here's the brilliant part — your app uses different navigation depending on screen size:
Small Screen (Phone)
Stack navigation. Tap a group → PUSH a new full screen. Swipe back or tap ← to POP. Each screen takes the whole display.
Large Screen (Tablet/Desktop)
Sidebar + Detail. Click a group in the sidebar → the detail panel updates in place. No pushing or popping — just like Apple Mail on iPad!
Both use the same widgets — just connected differently. The LayoutBuilder decides which pattern to use!
1 Add Navigation — Tap a Group, See Its Contacts!
For small screens (phones), tapping a contact group should push a new screen showing that group's contacts.
Step 1.1 The Navigator.push Pattern — Explained Like You're 5
Let's break down the navigation code word by word so you truly understand it:
🔍 Navigation Code — Every Word Explained
import 'contacts.dart'; // ← Import the contacts screen!
class ContactGroupsPage extends StatelessWidget {
const ContactGroupsPage({super.key});
@override
Widget build(BuildContext context) {
return _ContactGroupsView(
onListSelected: (list) => Navigator.of(context).push(
CupertinoPageRoute<void>(
title: list.title,
builder: (context) => ContactListsPage(listId: list.id),
),
),
);
}
}
Let me explain every single part like you're 5 years old:
onListSelected: (list) => ...— "When someone taps a contact group, here's what to do with that group..."Navigator.of(context)— "Find the navigator (the screen-stack-manager) that belongs to this part of the app." Think of it like finding the deck of cards that holds all your screens..push(...)— "Put a NEW card on top of the stack!" This is the action — it tells Flutter to show a new screen.CupertinoPageRoute<void>(...)— "Use the iOS-style animation (slide from right) for this new screen." The<void>means this screen doesn't return any data when it's popped.title: list.title— "The title for the navigation bar at the top." This shows "iPhone" or "Friends" etc.builder: (context) => ContactListsPage(listId: list.id)— "THIS is the actual screen to show! Build a ContactListsPage and give it the ID of the group that was tapped."
context?
context is like your app's "address" in the widget tree. Navigator.of(context) says:
"Starting from where I am in the tree, go UP until you find a Navigator." Every CupertinoApp
(and MaterialApp) automatically creates a Navigator at the top — you don't need to make one yourself!
Step 1.2 How to Go Back — The Pop Method
You don't need to write pop code for the back button! CupertinoPageRoute automatically:
- ✅ Adds a back button (←) in the navigation bar
- ✅ Enables the swipe-from-left-edge gesture to go back
- ✅ Calls
Navigator.pop()automatically when either is used
But if you ever need to programmatically go back (like from a "Save" button), you'd write:
Navigator.of(context).pop(); // "Remove the top card. Show the screen underneath."
2 Create the Sidebar — For Tablets and Desktops
On large screens, we don't push new screens. Instead, clicking a group in the sidebar updates the detail panel next to it.
Step 2.1 The Sidebar Widget — Same View, Different Callback
Add this widget to the bottom of lib/screens/contact_groups.dart:
🔍 ContactGroupsSidebar — Explained
class ContactGroupsSidebar extends StatelessWidget {
const ContactGroupsSidebar({
super.key,
required this.selectedListId, // Which group is highlighted?
required this.onListSelected, // What to do when a group is tapped
});
final int selectedListId;
final void Function(int) onListSelected;
@override
Widget build(BuildContext context) {
return _ContactGroupsView(
selectedListId: selectedListId, // ← Pass which group is selected
onListSelected: (list) => onListSelected(list.id), // ← Call with ID, not the whole list
);
}
}
What's different from the phone version?
- Phone version (ContactGroupsPage): The callback does
Navigator.push(...)— it navigates to a new screen. - Tablet version (ContactGroupsSidebar): The callback just calls
onListSelected(list.id)— it tells the parent "the user clicked group #3", and the parent updates the detail panel. No navigation happens! selectedListId— Tells the view which group is currently selected so it can highlight it (like showing a blue background).
This is the reusability magic of Flutter — the same _ContactGroupsView widget works for BOTH patterns, just with different callbacks!
3 Create the Detail View — No Back Button Needed
On large screens, the detail panel shouldn't show a back button — because you didn't "push" to get there!
Step 3.1 Hide the Back Button with automaticallyImplyLeading
Add this widget to the bottom of lib/screens/contacts.dart:
class ContactListDetail extends StatelessWidget {
const ContactListDetail({super.key, required this.listId});
final int listId;
@override
Widget build(BuildContext context) {
return _ContactListView(
listId: listId,
automaticallyImplyLeading: false, // ← Hide the back button!
);
}
}
What automaticallyImplyLeading: false means:
- When a screen is pushed onto the stack, Flutter automatically adds a back button (←) in the navigation bar. This is called the "leading" widget.
- But in our sidebar layout, the detail panel wasn't pushed — it's just sitting there. A back button would be confusing!
- Setting
automaticallyImplyLeading: falsetells Flutter: "Don't show a back button — the user navigates by clicking the sidebar instead."
4 Connect Sidebar + Detail in the Large Screen Layout
Now update the adaptive layout to use the sidebar and detail view together on large screens.
Step 4.1 The Complete Large Screen Layout
Update your adaptive_layout.dart with the complete large screen layout that has a sidebar AND detail panel:
🔍 The Master-Detail Pattern
Widget _buildLargeScreenLayout() {
return CupertinoPageScaffold(
backgroundColor: CupertinoColors.extraLightBackgroundGray,
child: SafeArea(
child: Row(
children: [
SizedBox(
width: 320, // Sidebar width
child: ContactGroupsSidebar( // ← The sidebar!
selectedListId: selectedListId, // Which group is highlighted
onListSelected: _onContactListSelected, // Update state on tap
),
),
Container( // ← Divider line
width: 1,
color: CupertinoColors.separator, // iOS-style thin gray line
),
Expanded( // ← Takes remaining space
child: ContactListDetail( // ← The detail panel!
listId: selectedListId, // Shows selected group
),
),
],
),
),
);
}
The layout structure (imagine it visually):
┌──────────────────────────────────────────────┐ │ ┌─────────────┐ ┌───────────────────────┐ │ │ │ │ │ │ │ │ │ Sidebar │ │ Detail Panel │ │ │ │ (320px) │ │ (Expanded) │ │ │ │ │ │ │ │ │ │ • iPhone │ │ Shows contacts │ │ │ │ • Friends │ │ for selected group │ │ │ │ • Work │ │ │ │ │ │ │ │ │ │ │ └─────────────┘ └───────────────────────┘ │ │ ↑ thin gray divider line │ └──────────────────────────────────────────────┘
5 The Complete Picture — Phone vs Tablet Navigation
Let's see the full flow of how your app decides which navigation pattern to use.
Step 5.1 Test Both Patterns
- Resize your browser to be narrow (like a phone)
- You see the full-screen contact groups list
- Tap "iPhone" → A NEW screen slides in from the right (iOS animation!)
- See the back button (← iPhone) at the top? Tap it → Slides back to the groups list
- Or swipe from the left edge → Same effect!
- This is stack navigation — push and pop!
- Resize your browser to be wide (like a tablet)
- You see a sidebar on the left AND a detail panel on the right
- Click "Friends" in the sidebar → The detail panel updates immediately!
- No screen push, no animation, no back button — the content just changes
- Click "Work" → Detail updates again
- This is master-detail navigation!
Same app, same code, two completely different navigation experiences! The LayoutBuilder checks the screen width and picks the right pattern automatically.
📝 What You Learned Today
Navigator.push & pop
Used Navigator.of(context).push() to add screens and .pop() to remove them — the foundation of all app navigation.
CupertinoPageRoute
iOS-style transitions: slide from right, automatic back button, swipe-to-go-back gesture support.
Sidebar + Detail Pattern
Built master-detail layout for tablets — sidebar updates detail panel without navigation stack.
Completed Rolodex App!
Built a complete iOS contacts app with adaptive layouts, slivers, search, AND navigation! 🎉
🧠 Test Yourself!
Q1 What does Navigator.of(context).push do?
Q2 What does Navigator.of(context).pop() do?
📦 Project 1: Navigate to a Contact Detail Screen
Objective: Tap a contact name to see their full details
📋 Requirements:
- Create a new screen:
lib/screens/contact_detail.dart - It should be a StatefulWidget that takes a
Contactobject - Show the contact's full name (with middle name and suffix if present) as a large title
- Show their first name, last name, and ID as detail rows
- In the contacts list, wrap each contact in a GestureDetector or use
onTap - When tapped, push the ContactDetailScreen using
CupertinoPageRoute - Use
automaticallyImplyLeading: true(default) to show the back button
🎯 Expected:
Tap a contact → a new screen slides in showing their full details → tap back or swipe to go back to the list.
📦 Project 2: Pass Data Back When Popping
Objective: Return a "favorited" status when going back
📋 Requirements:
- In
ContactDetailScreen, add a CupertinoButton that says "Favorite" - When the user taps "Favorite", call
Navigator.pop(context, true)— thetrueis the return value - If they just tap the back button, it pops with
null(default) - In the list screen, await the push and check the result:
final result = await Navigator.push(...)- If
result == true, show a "Added to favorites!" snackbar
- Use
showCupertinoDialogor a simpleprintto confirm it worked
🎯 Expected:
Navigate to a contact → tap "Favorite" → the screen pops back → the list screen shows a confirmation message!
onTap to be async, then: final favorited = await Navigator.of(context).push<bool>(...); — notice the <bool> tells Dart the return type!Lesson Complete!
You built a complete navigation system with different patterns for phones and tablets! The Rolodex app is finished — an iOS-style contacts app with adaptive layouts, slivers, search, and navigation!
Click the button above to track your progress!