Handling Multi-Screen Navigation in Flutter: A Practical Tutorial

Maxim Gorin
7 min readSep 17, 2024

--

Welcome to the next part of our beginner-friendly Flutter series! This series is designed specifically for newcomers to Flutter and Dart, and everything can be built directly in DartPad — no need to set up a complex IDE. In the last article, BuildingSmart Task Managers with Lists, Maps, and ListView in Flutter, we explored how to manage and display data efficiently. Now, we’re moving forward with a crucial aspect of mobile app development: navigation. In this post, you’ll learn how to navigate between screens, pass data, and manage multiple sections using BottomNavigationBar. Let’s get started!

Learn Flutter

Navigation is a core concept in mobile app development that allows users to move between different screens (or routes) and interact with various features. In Flutter, navigating between screens is straightforward, but it’s important to understand how it works, particularly when passing data between screens or managing multiple sections of an app using a BottomNavigationBar.

By the end of this article, you’ll have a solid understanding of navigation in Flutter, enabling you to build multi-screen apps with dynamic user interaction.

Understanding Navigation in Flutter

In Flutter, navigation is managed by the Navigator widget. This widget works like a stack of screens (or routes), where each new screen is pushed onto the stack when it’s opened, and when the user navigates back, the top screen is popped off.

The Navigator widget uses two primary methods:

  • push: Adds a new screen to the top of the navigation stack.
  • pop: Removes the top screen from the navigation stack, returning to the previous screen.

These actions allow you to move seamlessly between different screens while managing the app’s state. For example, if you have a home screen and a detail screen, the home screen will stay underneath the detail screen, and when you press “back,” you return to the home screen.

Basic Navigation: Push and Pop

Let’s start by understanding basic navigation using a book app example. The app will have a list of books on the main screen. When a user taps on a book, they navigate to a detail screen where they can view more information about the selected book.

Here’s how it looks in Flutter:

import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
home: BookListScreen(),
));
}

class BookListScreen extends StatelessWidget {
final List<Map<String, String>> books = [
{'title': '1984', 'author': 'George Orwell', 'description': 'A dystopian society under constant surveillance.'},
{'title': 'Brave New World', 'author': 'Aldous Huxley', 'description': 'A futuristic world driven by technology and control.'},
{'title': 'Fahrenheit 451', 'author': 'Ray Bradbury', 'description': 'A society where books are outlawed and burned.'},
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Book List')),
body: ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(books[index]['title']!),
subtitle: Text(books[index]['author']!),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BookDetailScreen(
title: books[index]['title']!,
author: books[index]['author']!,
description: books[index]['description']!,
),
),
);
},
);
},
),
);
}
}

class BookDetailScreen extends StatelessWidget {
final String title;
final String author;
final String description;

BookDetailScreen({required this.title, required this.author, required this.description});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Title: $title', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('Author: $author', style: TextStyle(fontSize: 18, fontStyle: FontStyle.italic)),
SizedBox(height: 12),
Text('Description:', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 6),
Text(description, style: TextStyle(fontSize: 16)),
],
),
),
);
}
}

What’s happening here?

Basic Navigation: Push and Pop
  1. BookListScreen: Displays a list of books. We’re using ListView.builder, which is optimal for rendering lists dynamically based on the number of items. Each ListTile represents a book, and tapping it triggers a navigation event.
  2. Navigator.push: This method pushes the BookDetailScreen onto the navigation stack, passing the book's data (title, author, and description) to display the detailed information. The new screen is pushed onto the stack, and the user can see the book’s details.
  3. Navigator.pop (not explicitly used here): When the user presses the back button, Navigator.pop is called implicitly, and the BookDetailScreen is popped off the stack, returning the user to the previous screen (BookListScreen).

Passing Data Between Screens

Navigation is not just about moving between screens — it often involves passing data between them. In our example, we’re passing the book’s title, author, and description from the list screen to the detail screen.

This is done through the constructor of the BookDetailScreen:

BookDetailScreen({required this.title, required this.author, required this.description});

By passing this data directly into the new screen’s constructor, we ensure that each screen has access to the necessary information. Flutter’s navigation system handles this smoothly, making data flow simple.

Managing Multiple Sections with BottomNavigationBar

Most apps have multiple sections that users can navigate between, such as a home screen, a profile page, and a settings screen. Flutter’s BottomNavigationBar is a great tool for managing this type of navigation.

For our book app, let’s expand it to allow users to:

  • Add books to the list manually.
  • Mark books as favorites.
  • View favorite books on a separate screen.

This will give us a more interactive app where users can switch between sections using a BottomNavigationBar.

Building an Interactive Book Management App

Now we’ll build the final version of our app where users can add books to the list, mark them as favorites, and navigate between the book list and favorite books using a BottomNavigationBar.

Complete Code:

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
int _currentIndex = 0;
final List<Map<String, dynamic>> books = [];
final List<Map<String, dynamic>> favoriteBooks = [];
final _titleController = TextEditingController();
final _authorController = TextEditingController();

// Adding a book to the list
void addBook(String title, String author) {
setState(() {
books.add({
'title': title,
'author': author,
'isFavorite': false,
});
});
}

// Toggling favorite status of a book
void toggleFavorite(int index) {
setState(() {
books[index]['isFavorite'] = !books[index]['isFavorite'];
if (books[index]['isFavorite']) {
favoriteBooks.add(books[index]);
} else {
favoriteBooks.removeWhere((book) => book['title'] == books[index]['title']);
}
});
}

// Removing a book from the list
void removeBook(int index) {
setState(() {
favoriteBooks.removeWhere((book) => book['title'] == books[index]['title']);
books.removeAt(index);
});
}

// Removing a book from the favorites list
void removeFavorite(int index) {
setState(() {
var book = favoriteBooks[index];
books.firstWhere((b) => b['title'] == book['title'])['isFavorite'] = false;
favoriteBooks.removeAt(index);
});
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Book Manager')),
body: _currentIndex == 0
? Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Book Title'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _authorController,
decoration: InputDecoration(labelText: 'Book Author'),
),
),
ElevatedButton(
onPressed: () {
addBook(_titleController.text, _authorController.text);
_titleController.clear();
_authorController.clear();
},
child: Text('Add Book'),
),
Expanded(
child: BookListScreen(
books: books,
onToggleFavorite: toggleFavorite,
onRemoveBook: removeBook,
),
),
],
)
: FavoriteBooksScreen (
favoriteBooks: favoriteBooks,
onRemoveFavorite: removeFavorite,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.book),
label: 'Books',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Favorites',
),
],
),
),
);
}
}

class BookListScreen extends StatelessWidget {
final List<Map<String, dynamic>> books;
final Function(int) onToggleFavorite;
final Function(int) onRemoveBook;

BookListScreen({required this.books, required this.onToggleFavorite, required this.onRemoveBook});

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: books.length,
itemBuilder: (context, index) {
return Dismissible(
key: Key(books[index]['title']),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
onRemoveBook(index);
},
child: ListTile(
title: Text(books[index]['title']),
subtitle: Text(books[index]['author']),
trailing: IconButton(
icon: Icon(
books[index]['isFavorite'] ? Icons.favorite : Icons.favorite_border,
color: books[index]['isFavorite'] ? Colors.red : null,
),
onPressed: () => onToggleFavorite(index),
),
),
);
},
);
}
}

class FavoriteBooksScreen extends StatelessWidget {
final List<Map<String, dynamic>> favoriteBooks;
final Function(int) onRemoveFavorite;

FavoriteBooksScreen({required this.favoriteBooks, required this.onRemoveFavorite});

@override
Widget build(BuildContext context) {
return favoriteBooks.isEmpty
? Center(child: Text('No favorite books yet.'))
: ListView.builder(
itemCount: favoriteBooks.length,
itemBuilder: (context, index) {
return Dismissible(
key: Key(favoriteBooks[index]['title']),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.symmetric(horizontal: 20),
child: Icon(Icons.delete, color: Colors.white),
),
onDismissed: (direction) {
onRemoveFavorite(index);
},
child: ListTile(
title: Text(favoriteBooks[index]['title']),
subtitle: Text(favoriteBooks[index]['author']),
trailing: IconButton(
icon: Icon(Icons.favorite, color: Colors.red),
onPressed: () => onRemoveFavorite(index),
),
),
);
},
);
}
}

What’s happening here?

Interactive Book Management App
  1. BottomNavigationBar: This widget allows users to switch between the book list and the favorites list. The currentIndex controls which screen is shown. We use setState to update the index when a different tab is selected.
  2. BookListScreen: Displays a list of books, allows users to mark a book as a favorite or remove it from the list using a swipe gesture.
  3. FavoriteBooksScreen: Displays only the books marked as favorites. Users can unmark them as favorites or swipe to remove them entirely.
  4. Dynamic State Management: Each time a book is added, removed, or marked as a favorite, we update the app’s state using setState, which ensures the UI is rebuilt with the latest data.

Conclusion

In this article, we’ve explored how to manage navigation in Flutter, pass data between screens, and use BottomNavigationBar to navigate between different sections of an app. By applying these techniques, you can create multi-screen apps with seamless navigation and dynamic user interaction.

This example of a book manager can be expanded further — perhaps by saving data locally or fetching data from an API — but it serves as a strong foundation for understanding the core principles of Flutter navigation.

Congrats on taking another step in your Flutter journey! Remember, learning to build multi-screen apps and handle navigation is a fundamental skill for any developer. Don’t be discouraged if things feel challenging at times — growth comes with practice. Keep pushing forward, share your progress, and don’t hesitate to leave your thoughts and successes in the comments. Let’s build a supportive community together! Be sure to subscribe for more tutorials, and I can’t wait to see where your coding takes you next!

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

No responses yet