Home Flutter UI 102 Stack Based Navigation

🧭 Stack Based Navigation — Move Between Screens Like a Pro!

⏱️ 35-40 minutes 📊 Advanced — Complete the app! 📦 2 Projects included 🏷️ Navigator, Routes, Push, Pop, Adaptive

🧒 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
🎯
Today you'll build TWO navigation patterns:
  • 📱 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();
🍎
CupertinoPageRoute makes the transition feel like iOS — the new screen slides in from the right, and you can swipe from the left edge to go back. It automatically adds a back button in the navigation bar too!
📱🆚💻

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

lib/screens/contact_groups.dart — Add navigation
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:

  1. onListSelected: (list) => ... — "When someone taps a contact group, here's what to do with that group..."
  2. 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.
  3. .push(...) — "Put a NEW card on top of the stack!" This is the action — it tells Flutter to show a new screen.
  4. 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.
  5. title: list.title — "The title for the navigation bar at the top." This shows "iPhone" or "Friends" etc.
  6. 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."
🧠
Why does Navigator need 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

Add to contact_groups.dart
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:

Add to 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: false tells 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

Large screen layout
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           │
└──────────────────────────────────────────────┘
This is called the "Master-Detail" pattern! It's used by Apple Mail, Notes, Contacts, and almost every iPad app. The sidebar is the "master" (controls what you see), and the detail panel is the "detail" (shows the content).

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

📱
Small Screen Test (Phone — under 600px wide):
  1. Resize your browser to be narrow (like a phone)
  2. You see the full-screen contact groups list
  3. Tap "iPhone" → A NEW screen slides in from the right (iOS animation!)
  4. See the back button (← iPhone) at the top? Tap it → Slides back to the groups list
  5. Or swipe from the left edge → Same effect!
  6. This is stack navigation — push and pop!
💻
Large Screen Test (Tablet/Desktop — 600px+ wide):
  1. Resize your browser to be wide (like a tablet)
  2. You see a sidebar on the left AND a detail panel on the right
  3. Click "Friends" in the sidebar → The detail panel updates immediately!
  4. No screen push, no animation, no back button — the content just changes
  5. Click "Work" → Detail updates again
  6. 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

Project 1Intermediate

Objective: Tap a contact name to see their full details

📋 Requirements:

  1. Create a new screen: lib/screens/contact_detail.dart
  2. It should be a StatefulWidget that takes a Contact object
  3. Show the contact's full name (with middle name and suffix if present) as a large title
  4. Show their first name, last name, and ID as detail rows
  5. In the contacts list, wrap each contact in a GestureDetector or use onTap
  6. When tapped, push the ContactDetailScreen using CupertinoPageRoute
  7. 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

Project 2Advanced

Objective: Return a "favorited" status when going back

📋 Requirements:

  1. In ContactDetailScreen, add a CupertinoButton that says "Favorite"
  2. When the user taps "Favorite", call Navigator.pop(context, true) — the true is the return value
  3. If they just tap the back button, it pops with null (default)
  4. 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
  5. Use showCupertinoDialog or a simple print to confirm it worked

🎯 Expected:

Navigate to a contact → tap "Favorite" → the screen pops back → the list screen shows a confirmation message!

💡
Hint: Change onTap to be async, then: final favorited = await Navigator.of(context).push<bool>(...); — notice the <bool> tells Dart the return type!

🚀 What's Next?

🎉 Congratulations! You've completed the "Flutter UI 102" topic AND the entire Rolodex app! You now know:

  • ✅ Adaptive layouts with LayoutBuilder
  • ✅ Advanced scrolling with slivers and sticky headers
  • ✅ CupertinoSliverNavigationBar with collapsing large titles
  • ✅ Real-time search with CupertinoSearchTextField
  • ✅ Stack-based navigation with push/pop
  • ✅ Master-detail pattern for tablets

In the final topic, you'll learn How Flutter Works — the engine, rendering pipeline, and architecture!

Next Lesson: How Flutter Works →
🎉

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!