Lesson 29 of 51 beginner

Navigation Basics

Moving Between Screens with Navigator

Open interactive version (quiz + challenge)

Real-world analogy

Navigator is like a stack of plates in a cafeteria. When you go to a new screen, you push a new plate on top of the stack. When you press the back button, you pop the top plate off and see the one underneath. You can only see the top plate (current screen) at any time, but all the previous screens are waiting underneath, exactly how you left them.

What is it?

Flutter's Navigator manages a stack of Route objects representing screens. Navigator.push adds a screen on top, Navigator.pop removes it, and MaterialPageRoute provides platform-appropriate animations. You can pass data forward through constructor parameters and backward through pop results. Named routes provide string-based navigation, while pushReplacement and pushAndRemoveUntil control the back stack.

Real-world relevance

In team_mvp_kit, basic Navigator is used for simple screen transitions before go_router is introduced. The login flow uses pushReplacement to prevent going back to login after authentication. The app uses pushAndRemoveUntil on logout to clear the entire navigation stack. Understanding Navigator basics is essential before learning go_router.

Key points

Code example

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  String _selectedFruit = 'None selected';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Selected: $_selectedFruit',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () async {
                final result = await Navigator.push<String>(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const FruitPickerScreen(),
                  ),
                );
                if (result != null) {
                  setState(() => _selectedFruit = result);
                }
              },
              child: const Text('Pick a Fruit'),
            ),
            const SizedBox(height: 12),
            ElevatedButton(
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const SettingsScreen(),
                  ),
                );
              },
              child: const Text('Settings'),
            ),
          ],
        ),
      ),
    );
  }
}

class FruitPickerScreen extends StatelessWidget {
  const FruitPickerScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final fruits = ['Apple', 'Banana', 'Cherry', 'Durian', 'Elderberry'];

    return Scaffold(
      appBar: AppBar(title: const Text('Pick a Fruit')),
      body: ListView.builder(
        itemCount: fruits.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(fruits[index]),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              // Pop and return the selected fruit
              Navigator.pop(context, fruits[index]);
            },
          );
        },
      ),
    );
  }
}

class SettingsScreen extends StatelessWidget {
  const SettingsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Simulate logout: clear stack and go to login
            Navigator.pushAndRemoveUntil(
              context,
              MaterialPageRoute(
                builder: (context) => const LoginScreen(),
              ),
              (route) => false,
            );
          },
          child: const Text('Logout'),
        ),
      ),
    );
  }
}

class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // After login, replace login with home
            Navigator.pushReplacement(
              context,
              MaterialPageRoute(
                builder: (context) => const HomeScreen(),
              ),
            );
          },
          child: const Text('Login'),
        ),
      ),
    );
  }
}

Line-by-line walkthrough

  1. 1. Import the Material package
  2. 2.
  3. 3. Define HomeScreen as a StatefulWidget
  4. 4.
  5. 5.
  6. 6. Create the State class
  7. 7. Track which fruit was selected, starting with none
  8. 8.
  9. 9. Build method creates the UI
  10. 10. Scaffold with Home title in AppBar
  11. 11. Center the content
  12. 12. Column with centered alignment
  13. 13.
  14. 14. Display the currently selected fruit
  15. 15. Style with headlineSmall from theme
  16. 16.
  17. 17. 24px gap
  18. 18. ElevatedButton to open the fruit picker
  19. 19. Await the result from Navigator.push
  20. 20. Push a MaterialPageRoute to FruitPickerScreen
  21. 21.
  22. 22.
  23. 23.
  24. 24. If a fruit was selected (not null), update state
  25. 25.
  26. 26.
  27. 27.
  28. 28. 12px gap
  29. 29. Another ElevatedButton for settings
  30. 30. Push to SettingsScreen (no result expected)
  31. 31.
  32. 32.
  33. 33.
  34. 34.
  35. 35.
  36. 36.
  37. 37.
  38. 38.
  39. 39.
  40. 40.
  41. 41. FruitPickerScreen is a StatelessWidget
  42. 42.
  43. 43.
  44. 44. Define a list of fruit names
  45. 45.
  46. 46. Scaffold with Pick a Fruit AppBar
  47. 47. ListView.builder for the fruit list
  48. 48. Build a ListTile for each fruit
  49. 49. Display the fruit name
  50. 50. Chevron icon as trailing indicator
  51. 51. On tap, pop with the selected fruit name
  52. 52. Navigator.pop passes the fruit string back to HomeScreen
  53. 53.
  54. 54.
  55. 55.
  56. 56.
  57. 57.
  58. 58. SettingsScreen is a StatelessWidget
  59. 59.
  60. 60. Scaffold with Settings AppBar
  61. 61. Logout button in the center
  62. 62. On press, clear the entire navigation stack
  63. 63. pushAndRemoveUntil pushes LoginScreen
  64. 64.
  65. 65.
  66. 66. (route) => false removes ALL previous routes
  67. 67.
  68. 68.
  69. 69.
  70. 70.
  71. 71.
  72. 72.
  73. 73. LoginScreen is a StatelessWidget
  74. 74.
  75. 75. Scaffold with Login AppBar
  76. 76. Login button in the center
  77. 77. On press, replace login screen with home
  78. 78. pushReplacement ensures user cannot go back to login
  79. 79.
  80. 80.
  81. 81.
  82. 82.
  83. 83.
  84. 84.

Spot the bug

ElevatedButton(
  onPressed: () {
    final result = Navigator.push<String>(
      context,
      MaterialPageRoute(
        builder: (context) => SelectionScreen(),
      ),
    );
    setState(() => selected = result);
  },
  child: Text('Select'),
)
Need a hint?
Navigator.push returns a Future. What are you missing?
Show answer
Navigator.push returns a Future<String?>, not a String directly. The code assigns the Future itself to result without awaiting it. Fix: mark the onPressed callback as async and await the push: final result = await Navigator.push<String>(...); Then check if result != null before calling setState.

Explain like I'm 5

Imagine you are reading a choose-your-own-adventure book. When you go to a new chapter (screen), you stick your finger in the current page (push) so you can come back. When you want to go back, you flip to where your finger was (pop). Navigator.push is putting your finger in and flipping to a new chapter. Navigator.pop is going back to where your finger was. And if you want to start the whole book over (like logging out), pushAndRemoveUntil takes out all your fingers and starts from chapter 1!

Fun fact

Flutter's Navigator 1.0 (imperative push/pop) was the original navigation system. Navigator 2.0 was added in Flutter 1.22 for declarative, URL-driven navigation — but it was so complex that the community created packages like go_router and auto_route to simplify it. go_router was eventually adopted by the Flutter team as the recommended solution!

Hands-on challenge

Build a three-screen flow: HomeScreen shows a message and two buttons (Go to Detail, Go to Settings). DetailScreen accepts a title parameter and has a back button that pops with a result string. SettingsScreen has a Logout button that uses pushAndRemoveUntil to clear the stack and navigate to a LoginScreen. Add a PopScope to the DetailScreen that shows a confirmation dialog before popping.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart