Flutter for Beginners: Navigation and Routing Between Screens

Capítulo 7

Estimated reading time: 7 minutes

+ Exercise

Why navigation matters in Flutter

Most real apps have more than one screen: a list of items, a detail view, and a screen to add or edit data. In Flutter, navigation is usually handled with a stack: when you open a new screen, it is pushed on top; when you go back, it is popped off. The main tool is Navigator, and you can navigate either with direct routes (creating a MaterialPageRoute) or with named routes (a string route name mapped to a widget builder).

The mental model: a stack of routes

  • Push: open a new screen by adding a route to the stack.
  • Pop: close the current screen by removing the top route.
  • Passing data: provide constructor arguments to the next screen (or use named route arguments).
  • Returning results: push returns a Future; when the pushed screen calls pop(result), the previous screen receives that result.

Step-by-step: build a 3-screen mini app

You will build a tiny “Notes” flow with three screens:

  • NotesListScreen: shows a list of notes and a button to add.
  • NoteDetailScreen: shows one note and allows editing.
  • EditNoteScreen: edits or creates a note and returns the updated text.

Step 1: Define a simple model

Keep the data structure minimal so you can focus on navigation. This example uses an in-memory list.

class Note {  final int id;  String text;  Note({required this.id, required this.text});}

Step 2: Organize routes in one place (named routes)

Named routes help keep navigation consistent and avoid scattering route strings throughout your code. Create a small routes helper and a single onGenerateRoute function to handle arguments.

class AppRoutes {  static const notes = '/';  static const detail = '/detail';  static const edit = '/edit';}
Route<dynamic> onGenerateRoute(RouteSettings settings) {  switch (settings.name) {    case AppRoutes.notes:      return MaterialPageRoute(builder: (_) => NotesListScreen());    case AppRoutes.detail:      final note = settings.arguments as Note;      return MaterialPageRoute(builder: (_) => NoteDetailScreen(note: note));    case AppRoutes.edit:      final args = settings.arguments as EditArgs;      return MaterialPageRoute(builder: (_) => EditNoteScreen(args: args));    default:      return MaterialPageRoute(        builder: (_) => const Scaffold(          body: Center(child: Text('Route not found')),        ),      );  }}

For edit, we’ll pass a small argument object so the screen can handle both “add” and “edit” modes cleanly.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

class EditArgs {  final String initialText;  final String title;  const EditArgs({required this.initialText, required this.title});}

Step 3: Wire routes into MaterialApp

Use initialRoute and onGenerateRoute. This keeps route creation centralized and supports typed arguments.

class MyApp extends StatelessWidget {  const MyApp({super.key});  @override  Widget build(BuildContext context) {    return MaterialApp(      initialRoute: AppRoutes.notes,      onGenerateRoute: onGenerateRoute,    );  }}

Screen 1: Notes list (push to detail, push to add)

This screen demonstrates two navigation styles:

  • Direct route push (inline MaterialPageRoute) for quick navigation.
  • Named route push for organized navigation with arguments.

Below, we’ll use named routes to open detail and edit screens, and we’ll also show a direct push example.

class NotesListScreen extends StatefulWidget {  @override  State<NotesListScreen> createState() => _NotesListScreenState();}class _NotesListScreenState extends State<NotesListScreen> {  final List<Note> _notes = [    Note(id: 1, text: 'Buy milk'),    Note(id: 2, text: 'Read Flutter docs'),  ];  int _nextId = 3;  Future<void> _addNote() async {    final result = await Navigator.pushNamed(      context,      AppRoutes.edit,      arguments: const EditArgs(initialText: '', title: 'Add Note'),    );    if (result is String && result.trim().isNotEmpty) {      setState(() {        _notes.add(Note(id: _nextId++, text: result.trim()));      });    }  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: const Text('Notes')),      floatingActionButton: FloatingActionButton(        onPressed: _addNote,        child: const Icon(Icons.add),      ),      body: ListView.builder(        itemCount: _notes.length,        itemBuilder: (context, index) {          final note = _notes[index];          return ListTile(            title: Text(note.text),            onTap: () async {              await Navigator.pushNamed(                context,                AppRoutes.detail,                arguments: note,              );              setState(() {});            },          );        },      ),    );  }}

Direct push example (MaterialPageRoute)

If you want a quick one-off navigation without route names, you can push a route directly. This is functionally fine for small apps, but named routes scale better.

Navigator.push(  context,  MaterialPageRoute(builder: (_) => NoteDetailScreen(note: note)),);

Screen 2: Detail screen (pop, push to edit, return result)

The detail screen receives a Note via its constructor. It can navigate to the edit screen and wait for a result. When the edit screen returns a new text value, the detail screen updates the note and refreshes.

class NoteDetailScreen extends StatefulWidget {  final Note note;  const NoteDetailScreen({super.key, required this.note});  @override  State<NoteDetailScreen> createState() => _NoteDetailScreenState();}class _NoteDetailScreenState extends State<NoteDetailScreen> {  Future<void> _edit() async {    final result = await Navigator.pushNamed(      context,      AppRoutes.edit,      arguments: EditArgs(initialText: widget.note.text, title: 'Edit Note'),    );    if (result is String && result.trim().isNotEmpty) {      setState(() {        widget.note.text = result.trim();      });    }  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(        title: const Text('Detail'),        leading: IconButton(          icon: const Icon(Icons.arrow_back),          onPressed: () => Navigator.pop(context),        ),      ),      body: Padding(        padding: const EdgeInsets.all(16),        child: Column(          crossAxisAlignment: CrossAxisAlignment.start,          children: [            Text(widget.note.text, style: Theme.of(context).textTheme.headlineSmall),            const SizedBox(height: 16),            ElevatedButton(              onPressed: _edit,              child: const Text('Edit'),            ),          ],        ),      ),    );  }}

Notice the pattern:

  • final result = await Navigator.push... waits for the next screen to close.
  • The edit screen will close with Navigator.pop(context, updatedText).
  • After the await, you update state based on the returned value.

Screen 3: Edit/Add screen (return a value with pop)

This screen is responsible for producing a result. It receives initial text and a title via EditArgs. When the user taps Save, it returns the text to the previous screen.

class EditNoteScreen extends StatefulWidget {  final EditArgs args;  const EditNoteScreen({super.key, required this.args});  @override  State<EditNoteScreen> createState() => _EditNoteScreenState();}class _EditNoteScreenState extends State<EditNoteScreen> {  late final TextEditingController _controller;  @override  void initState() {    super.initState();    _controller = TextEditingController(text: widget.args.initialText);  }  @override  void dispose() {    _controller.dispose();    super.dispose();  }  void _save() {    Navigator.pop(context, _controller.text);  }  @override  Widget build(BuildContext context) {    return Scaffold(      appBar: AppBar(title: Text(widget.args.title)),      body: Padding(        padding: const EdgeInsets.all(16),        child: Column(          children: [            TextField(              controller: _controller,              decoration: const InputDecoration(labelText: 'Note text'),            ),            const SizedBox(height: 16),            Row(              children: [                Expanded(                  child: ElevatedButton(                    onPressed: _save,                    child: const Text('Save'),                  ),                ),                const SizedBox(width: 12),                Expanded(                  child: OutlinedButton(                    onPressed: () => Navigator.pop(context),                    child: const Text('Cancel'),                  ),                ),              ],            ),          ],        ),      ),    );  }}

Passing data: constructor vs named route arguments

There are two common ways to pass data to a screen:

  • Constructor arguments: best when you create the route directly with MaterialPageRoute. Example: NoteDetailScreen(note: note).
  • Named route arguments: best when you centralize route creation. Example: Navigator.pushNamed(context, AppRoutes.detail, arguments: note), then read it in onGenerateRoute.

In both cases, you should keep the data types explicit and predictable. If you pass multiple values, prefer a small argument class (like EditArgs) instead of a loosely-typed Map.

Returning results: the key pattern

Returning results is what makes multi-screen flows feel connected (add/edit flows, pickers, confirmations). The pattern is:

  • Screen A: final result = await Navigator.push(...)
  • Screen B: Navigator.pop(context, result)
  • Screen A: handle result and update state
// Screen Aawait Navigator.pushNamed(context, AppRoutes.edit, arguments: ...);// Screen BNavigator.pop(context, 'new value');

Simple route organization tips

  • Keep route names in one class (like AppRoutes) to avoid typos.
  • Use onGenerateRoute when you need typed arguments and a single place to build routes.
  • Keep screens focused: list shows collection, detail shows one item, edit returns a value.
  • Be consistent: either mostly named routes or mostly direct routes; mixing is fine, but consistency helps maintainability.

Practice task: implement a detail page that returns a value

Goal: practice both navigation and returning results by updating the list screen after viewing details.

Task requirements

  • Create a ProductListScreen that shows a list of product names.
  • Create a ProductDetailScreen that shows the product name and a button: “Mark as Favorite”.
  • When the user taps “Mark as Favorite”, the detail screen should pop and return true.
  • Back on the list screen, if the result is true, update that product row (for example, append “★” to the name or show a trailing icon).

Suggested implementation steps

  • In the list screen, use final result = await Navigator.push(...) when opening the detail screen.
  • In the detail screen, call Navigator.pop(context, true) when the button is pressed.
  • In the list screen, check if (result == true) and call setState to update the UI.

Starter code skeleton

// In ProductListScreen onTap:final result = await Navigator.push(  context,  MaterialPageRoute(    builder: (_) => ProductDetailScreen(productName: name),  ),);if (result == true) {  setState(() {    // mark favorite in your list data  });}// In ProductDetailScreen button:onPressed: () {  Navigator.pop(context, true);}

Now answer the exercise about the content:

In a two-screen Flutter flow where Screen A opens Screen B to edit data, what should Screen A do to receive and use the updated value when Screen B closes?

You are right! Congratulations, now go to the next page

You missed! Try again.

In Flutter, push returns a Future. Screen A should await it, and Screen B should close with Navigator.pop(context, result). After the await, Screen A checks the result and calls setState to update the UI.

Next chapter

Flutter for Beginners: State Basics with StatefulWidget and setState

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
58%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.