Flutter Bottom Navigation: Sending Information from One Screen to Another
Bottom navigation is commonly used in Flutter applications to switch between important sections such as Home, Products, Cart, Profile, and Settings. In modern Material 3 Flutter applications, NavigationBar is the recommended Material widget for persistent navigation between primary destinations. Flutter also provides BottomNavigationBar for older Material 2 implementations. Flutter Material Widgets Documentation
When using bottom navigation, different sections often need to share or receive information. For example, a product selected on the Home tab may need to be displayed on the Cart tab, or a selected category may need to affect the Products tab.
1. What Is Bottom Navigation?
Bottom navigation is a persistent navigation component placed at the bottom of the application interface. It allows users to switch between the main sections of an application.
A typical application may have:
- Home
- Products
- Cart
- Orders
- Profile
Conceptually, the structure looks like this:
Flutter App
|
+-----------+-----------+
| | |
Home Products Profile
| | |
+-----------+-----------+
|
Bottom Navigation
2. NavigationBar vs BottomNavigationBar
Flutter's Material 3 design uses NavigationBar. Flutter's Material 3 migration documentation recommends replacing the older Material 2 BottomNavigationBar with NavigationBar in Material 3 applications. Flutter Material 3 Migration Guide
Widget |
Design System |
Typical Usage |
|---|
NavigationBar |
Material 3 |
Modern Flutter applications |
BottomNavigationBar |
Material 2 |
Existing or older applications |
CupertinoTabBar |
Cupertino/iOS |
iOS-style applications |
3. Basic Bottom Navigation Using NavigationBar
The selected bottom-navigation item is normally represented by an integer index. When the user selects another destination, setState() can update the selected index.
import 'package:flutter/material.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
final List<Widget> screens = const [
HomeScreen(),
ProductsScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: screens[selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
selectedIcon: Icon(Icons.shopping_bag),
label: 'Products',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile',
),
],
),
);
}
}
4. Creating Individual Screens
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Home Screen',
style: TextStyle(fontSize: 24),
),
);
}
}
class ProductsScreen extends StatelessWidget {
const ProductsScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Products Screen',
style: TextStyle(fontSize: 24),
),
);
}
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Profile Screen',
style: TextStyle(fontSize: 24),
),
);
}
}
5. What Does Sending Information Mean in Bottom Navigation?
Bottom navigation changes the currently displayed destination. However, applications often need data to move between those destinations.
For example:
Home
|
| Selected Product
v
Products
|
| Add Product
v
Cart
|
| User Information
v
Profile
There are several ways to handle this information:
- Pass data directly through widget constructors.
- Keep shared state in a common parent widget.
- Use callbacks to send information upward.
- Use a shared model or state-management solution.
- Use a route when a destination should open as a separate screen.
6. Passing Data Through a Common Parent
One of the simplest approaches for a small application is to keep shared data in the widget that owns the bottom navigation and pass that data to the selected screen.
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
String selectedProduct = 'Flutter Course';
@override
Widget build(BuildContext context) {
final screens = [
HomeScreen(
onProductSelected: (product) {
setState(() {
selectedProduct = product;
});
},
),
ProductsScreen(
selectedProduct: selectedProduct,
),
ProfileScreen(
selectedProduct: selectedProduct,
),
];
return Scaffold(
body: screens[selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Products',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
label: 'Profile',
),
],
),
);
}
}
7. Sending Information From Home to Products
Suppose the Home screen contains a button for selecting a course. The selected course can be sent back to the parent through a callback.
class HomeScreen extends StatelessWidget {
final ValueChanged<String> onProductSelected;
const HomeScreen({
super.key,
required this.onProductSelected,
});
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
onProductSelected('Flutter Course');
},
child: const Text('Select Flutter Course'),
),
);
}
}
The parent receives the selected value:
HomeScreen(
onProductSelected: (product) {
setState(() {
selectedProduct = product;
});
},
)
The Products screen can then receive the updated value:
class ProductsScreen extends StatelessWidget {
final String selectedProduct;
const ProductsScreen({
super.key,
required this.selectedProduct,
});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Selected: $selectedProduct',
style: const TextStyle(fontSize: 22),
),
);
}
}
8. Sending a Custom Object Between Bottom Navigation Screens
For real applications, passing a complete object is often more useful than passing individual values.
Product Model
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
}
Store the Selected Product
Product? selectedProduct;
Pass It to Another Destination
ProductsScreen(
product: selectedProduct,
)
Receive It
class ProductsScreen extends StatelessWidget {
final Product? product;
const ProductsScreen({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
if (product == null) {
return const Center(
child: Text('No product selected'),
);
}
return Center(
child: Text(
'${product!.name} - ₹${product!.price}',
),
);
}
}
9. Passing Information From Home to Cart
A common example is adding an item on the Home or Products tab and displaying it in the Cart tab.
class CartItem {
final String name;
final double price;
const CartItem({
required this.name,
required this.price,
});
}
Maintain the cart in the parent:
final List<CartItem> cartItems = [];
Add an item:
setState(() {
cartItems.add(
const CartItem(
name: 'Flutter Course',
price: 4999,
),
);
});
Pass the cart to the Cart screen:
CartScreen(
items: cartItems,
)
Cart Screen
class CartScreen extends StatelessWidget {
final List<CartItem> items;
const CartScreen({
super.key,
required this.items,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.name),
subtitle: Text(
'₹${item.price.toStringAsFixed(0)}',
),
);
},
);
}
}
10. Complete Bottom Navigation With Shared Data
The following example demonstrates a Home, Products, Cart, and Profile navigation structure. The selected product is stored in the parent and can be accessed by other destinations.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Bottom Navigation',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
Product? selectedProduct;
final List<Product> products = const [
Product(
id: 1,
name: 'Flutter Course',
price: 4999,
),
Product(
id: 2,
name: 'Dart Course',
price: 3999,
),
Product(
id: 3,
name: 'UI/UX Course',
price: 2999,
),
];
@override
Widget build(BuildContext context) {
final screens = [
HomeScreen(
onProductSelected: (product) {
setState(() {
selectedProduct = product;
});
},
),
ProductsScreen(
products: products,
selectedProduct: selectedProduct,
onProductSelected: (product) {
setState(() {
selectedProduct = product;
});
},
),
CartScreen(
selectedProduct: selectedProduct,
),
ProfileScreen(
selectedProduct: selectedProduct,
),
];
return Scaffold(
appBar: AppBar(
title: const Text('Flutter App'),
),
body: screens[selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
selectedIcon: Icon(Icons.shopping_bag),
label: 'Products',
),
NavigationDestination(
icon: Icon(Icons.shopping_cart_outlined),
selectedIcon: Icon(Icons.shopping_cart),
label: 'Cart',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile',
),
],
),
);
}
}
class HomeScreen extends StatelessWidget {
final ValueChanged<Product> onProductSelected;
const HomeScreen({
super.key,
required this.onProductSelected,
});
@override
Widget build(BuildContext context) {
const product = Product(
id: 1,
name: 'Flutter Course',
price: 4999,
);
return Center(
child: ElevatedButton(
onPressed: () {
onProductSelected(product);
},
child: const Text('Select Flutter Course'),
),
);
}
}
class ProductsScreen extends StatelessWidget {
final List<Product> products;
final Product? selectedProduct;
final ValueChanged<Product> onProductSelected;
const ProductsScreen({
super.key,
required this.products,
required this.selectedProduct,
required this.onProductSelected,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text(
'₹${product.price.toStringAsFixed(0)}',
),
trailing: product.id == selectedProduct?.id
? const Icon(Icons.check)
: null,
onTap: () {
onProductSelected(product);
},
);
},
);
}
}
class CartScreen extends StatelessWidget {
final Product? selectedProduct;
const CartScreen({
super.key,
required this.selectedProduct,
});
@override
Widget build(BuildContext context) {
if (selectedProduct == null) {
return const Center(
child: Text('Cart is empty'),
);
}
return Center(
child: Text(
'Cart: ${selectedProduct!.name}',
style: const TextStyle(fontSize: 22),
),
);
}
}
class ProfileScreen extends StatelessWidget {
final Product? selectedProduct;
const ProfileScreen({
super.key,
required this.selectedProduct,
});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
selectedProduct == null
? 'No product selected'
: 'Selected Product: ${selectedProduct!.name}',
style: const TextStyle(fontSize: 20),
),
);
}
}
11. Important Concept: Bottom Navigation Is Not the Same as Push Navigation
It is important to understand the difference between switching bottom-navigation destinations and pushing a completely new route.
Bottom Navigation |
Navigator.push() |
|---|
Switches between primary destinations |
Opens a new route |
Usually remains visible |
Usually replaced by the new screen |
Uses selected index or navigation state |
Uses Navigator route stack |
Suitable for Home, Cart, Profile, Settings |
Suitable for Details, Edit, Checkout, Forms |
Often keeps top-level navigation persistent |
Creates a navigation stack entry |
Flutter's navigation system uses the Navigator as a stack of routes, while NavigationBar is intended for switching between primary destinations. Flutter Navigation and Routing
12. Opening a Detail Screen From Bottom Navigation
Sometimes a bottom-navigation destination contains a list, and tapping an item should open a separate detail route. In this case, bottom navigation and normal Navigator-based navigation can be used together.
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
}
The flow becomes:
Bottom Navigation
|
v
Products Tab
|
| Tap Product
v
Product Details Route
|
| Back
v
Products Tab
This is useful when the detail page should temporarily appear above the bottom-navigation interface.
13. Sending Data to a Detail Screen From a Bottom Navigation Tab
class ProductsScreen extends StatelessWidget {
const ProductsScreen({super.key});
@override
Widget build(BuildContext context) {
final product = const Product(
id: 10,
name: 'Flutter Training',
price: 4999,
);
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
},
child: const Text('View Product'),
),
),
);
}
}
class ProductDetailsScreen extends StatelessWidget {
final Product product;
const ProductDetailsScreen({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Center(
child: Text(
'₹${product.price}',
style: const TextStyle(fontSize: 24),
),
),
);
}
}
14. Returning Information From a Detail Screen
A detail screen can return a result to the bottom-navigation tab using Navigator.pop().
Navigator.pop(
context,
'Product Added',
);
The Products tab can wait for the result:
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
if (result != null) {
print(result);
}
Flutter's official navigation documentation uses this push/pop pattern to return information from one route to another. Flutter: Return Data From a Screen
15. Passing Data With Callbacks
A callback is a function passed from a parent widget to a child widget. The child can call that function when information needs to be sent back to the parent.
Parent
HomeScreen(
onSelected: (value) {
setState(() {
selectedValue = value;
});
},
)
Child
class HomeScreen extends StatelessWidget {
final ValueChanged<String> onSelected;
const HomeScreen({
super.key,
required this.onSelected,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
onSelected('Flutter');
},
child: const Text('Select'),
);
}
}
Callbacks are useful when the data only needs to travel from a child destination back to the widget that owns the bottom-navigation state.
16. Sending Data Between Tabs Using a Shared Parent
When several tabs need access to the same data, the parent widget can own that state.
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
int cartCount = 0;
String? selectedCategory;
void addToCart() {
setState(() {
cartCount++;
});
}
void selectCategory(String category) {
setState(() {
selectedCategory = category;
});
}
}
Now different destinations can receive the same data:
HomeScreen(
onAddToCart: addToCart,
);
ProductsScreen(
selectedCategory: selectedCategory,
);
CartScreen(
cartCount: cartCount,
);
17. Bottom Navigation With a Shared Cart Counter
A common application requirement is displaying the number of items in the cart while switching between Home, Products, and Profile.
int cartCount = 0;
Update the count:
setState(() {
cartCount++;
});
Display the count in the navigation destination:
NavigationDestination(
icon: Badge(
label: Text('$cartCount'),
child: const Icon(
Icons.shopping_cart_outlined,
),
),
selectedIcon: Badge(
label: Text('$cartCount'),
child: const Icon(
Icons.shopping_cart,
),
),
label: 'Cart',
)
This pattern allows the parent navigation widget to control the shared cart state while the Cart tab displays the corresponding information.
18. Bottom Navigation With a User Object
Suppose a logged-in user needs to be available to Home, Orders, and Profile.
class User {
final int id;
final String name;
final String email;
const User({
required this.id,
required this.name,
required this.email,
});
}
Create the user:
const user = User(
id: 101,
name: 'Manish',
email: '[email protected]',
);
Pass it to different tabs:
HomeScreen(
user: user,
);
OrdersScreen(
user: user,
);
ProfileScreen(
user: user,
);
19. State Management for Larger Bottom Navigation Applications
For a small application, keeping shared information in the parent of the bottom-navigation destinations can be sufficient. As the application grows, multiple screens may need to read and update the same state.
Flutter's state-management documentation explains approaches for managing data shared across screens and throughout an application. Flutter State Management Documentation
Common state-management approaches include:
- Provider
- Riverpod
- Bloc/Cubit
- ChangeNotifier
- ValueNotifier
- InheritedWidget
- Other application-specific state architectures
The basic principle is:
Shared Application State
|
+--------------+--------------+
| | |
Home Cart Profile
| | |
+--------------+--------------+
|
Bottom Navigation
20. Using IndexedStack With Bottom Navigation
An IndexedStack can be useful when you want to keep multiple tab widgets in the widget tree while displaying only the selected one.
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
final screens = const [
HomeScreen(),
ProductsScreen(),
CartScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: selectedIndex,
children: screens,
),
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
label: 'Products',
),
NavigationDestination(
icon: Icon(Icons.shopping_cart_outlined),
label: 'Cart',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
label: 'Profile',
),
],
),
);
}
}
IndexedStack displays one child at a time based on the index while keeping the other children in the widget tree, which can be useful for preserving tab-specific widget state.
21. Nested Navigation With Bottom Navigation
More complex applications may require each bottom-navigation tab to have its own navigation stack.
For example:
Home Tab
|
+-- Home
+-- Product Details
+-- Checkout
Profile Tab
|
+-- Profile
+-- Edit Profile
+-- Settings
In such an architecture, each primary destination can have its own navigation history. Flutter's navigation documentation notes that applications with multiple Navigator widgets are among the scenarios where Router-based navigation can be useful. Flutter Navigation and Routing
22. Bottom Navigation With Named Routes
Named routes can be used for navigation, although Flutter currently does not recommend named routes for most new applications. For learning or existing applications, they may still be encountered.
MaterialApp(
routes: {
'/home': (context) => const HomeScreen(),
'/products': (context) => const ProductsScreen(),
'/profile': (context) => const ProfileScreen(),
},
);
Navigation:
Navigator.pushNamed(
context,
'/products',
);
Flutter's documentation recommends considering go_router or Navigator with MaterialPageRoute for most new applications instead of relying on named routes. Flutter Named Routes Documentation
23. Passing Arguments With Named Routes
If a named route is used, information can be passed through the arguments parameter.
Navigator.pushNamed(
context,
'/product-details',
arguments: product,
);
The destination can read the argument:
final product =
ModalRoute.of(context)!.settings.arguments as Product;
Flutter provides an official example for passing arguments to named routes. Pass Arguments to a Named Route
24. Bottom Navigation and Deep Linking
When an application needs URLs that directly open specific content, such as a product or profile page, navigation becomes more complex.
For example:
myapp.com/home
myapp.com/products
myapp.com/products/101
myapp.com/profile
Flutter supports deep linking, and applications with advanced navigation and deep-linking requirements can use Router-based navigation or a routing package such as go_router. Flutter Deep Linking Documentation
25. NavigationBar With Different Icons
NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
NavigationDestination(
icon: Icon(Icons.search_outlined),
selectedIcon: Icon(Icons.search),
label: 'Search',
),
NavigationDestination(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: 'Favorites',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile',
),
],
)
26. Passing Search Information Between Tabs
Suppose a user searches for a product in the Home tab and the Products tab should display the selected search query.
String searchQuery = '';
void updateSearch(String query) {
setState(() {
searchQuery = query;
});
}
Home:
HomeScreen(
onSearch: updateSearch,
)
Products:
ProductsScreen(
searchQuery: searchQuery,
)
Products screen:
class ProductsScreen extends StatelessWidget {
final String searchQuery;
const ProductsScreen({
super.key,
required this.searchQuery,
});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
searchQuery.isEmpty
? 'All Products'
: 'Searching for: $searchQuery',
),
);
}
}
27. Practical Example: Home to Cart
Consider an e-commerce application with four bottom-navigation destinations:
Home | Products | Cart | Profile
When a product is selected on Home:
Home
|
| Product
v
Parent State
|
v
Cart
The parent maintains the cart:
final List<Product> cart = [];
Add the selected product:
void addToCart(Product product) {
setState(() {
cart.add(product);
});
}
Pass it to Cart:
CartScreen(
cart: cart,
)
This creates a simple one-way data flow that is easy to understand in small applications.
28. Practical Example: Profile Information Across Tabs
class User {
final String name;
final String email;
const User({
required this.name,
required this.email,
});
}
Store the user at the parent level:
const user = User(
name: 'Manish',
email: '[email protected]',
);
Pass the user to multiple tabs:
HomeScreen(user: user);
OrdersScreen(user: user);
ProfileScreen(user: user);
This avoids duplicating the same user data in each screen.
29. Complete Bottom Navigation Example With Data Transfer
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Bottom Navigation Data',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int selectedIndex = 0;
final List<Product> cart = [];
final products = const [
Product(
id: 1,
name: 'Flutter Course',
price: 4999,
),
Product(
id: 2,
name: 'Dart Course',
price: 3999,
),
Product(
id: 3,
name: 'UI/UX Course',
price: 2999,
),
];
void addToCart(Product product) {
setState(() {
cart.add(product);
});
}
@override
Widget build(BuildContext context) {
final screens = [
HomeScreen(
products: products,
onAddToCart: addToCart,
),
ProductsScreen(
products: products,
onAddToCart: addToCart,
),
CartScreen(
cart: cart,
),
const ProfileScreen(),
];
return Scaffold(
appBar: AppBar(
title: const Text('My Flutter App'),
),
body: screens[selectedIndex],
bottomNavigationBar: NavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
destinations: [
const NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: 'Home',
),
const NavigationDestination(
icon: Icon(Icons.shopping_bag_outlined),
selectedIcon: Icon(Icons.shopping_bag),
label: 'Products',
),
NavigationDestination(
icon: Badge(
isLabelVisible: cart.isNotEmpty,
label: Text('${cart.length}'),
child: const Icon(
Icons.shopping_cart_outlined,
),
),
selectedIcon: Badge(
isLabelVisible: cart.isNotEmpty,
label: Text('${cart.length}'),
child: const Icon(
Icons.shopping_cart,
),
),
label: 'Cart',
),
const NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: 'Profile',
),
],
),
);
}
}
class HomeScreen extends StatelessWidget {
final List<Product> products;
final ValueChanged<Product> onAddToCart;
const HomeScreen({
super.key,
required this.products,
required this.onAddToCart,
});
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Featured Products',
style: Theme.of(context)
.textTheme
.headlineSmall,
),
const SizedBox(height: 15),
...products.map(
(product) {
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text(
'₹${product.price.toStringAsFixed(0)}',
),
trailing: ElevatedButton(
onPressed: () {
onAddToCart(product);
},
child: const Text('Add'),
),
),
);
},
),
],
);
}
}
class ProductsScreen extends StatelessWidget {
final List<Product> products;
final ValueChanged<Product> onAddToCart;
const ProductsScreen({
super.key,
required this.products,
required this.onAddToCart,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text(
'₹${product.price.toStringAsFixed(0)}',
),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () {
onAddToCart(product);
},
),
);
},
);
}
}
class CartScreen extends StatelessWidget {
final List<Product> cart;
const CartScreen({
super.key,
required this.cart,
});
@override
Widget build(BuildContext context) {
if (cart.isEmpty) {
return const Center(
child: Text(
'Your cart is empty',
style: TextStyle(fontSize: 20),
),
);
}
return ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
final product = cart[index];
return ListTile(
leading: const Icon(
Icons.shopping_bag,
),
title: Text(product.name),
subtitle: Text(
'₹${product.price.toStringAsFixed(0)}',
),
);
},
);
}
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'Profile Screen',
style: TextStyle(fontSize: 24),
),
);
}
}
How This Example Works
MainScreen owns the bottom-navigation state.
- The parent also owns the cart list.
- Home and Products receive the
onAddToCart callback.
- When a product is selected, the child calls the callback.
- The parent updates the cart using
setState().
- The updated cart is passed to the Cart destination.
- The NavigationBar badge displays the number of cart items.
- Switching to Cart displays the products added from another destination.
30. Best Practices for Bottom Navigation Data Transfer
- Keep shared state at the lowest common parent that needs to coordinate the data.
- Use constructor parameters for straightforward data transfer.
- Use callbacks when child screens need to notify the parent.
- Use model classes for structured application data.
- Use state management when many unrelated widgets need the same changing data.
- Use
NavigationBar for modern Material 3 applications.
- Use
Navigator.push() for temporary/detail routes opened from a bottom-navigation destination.
- Use
Navigator.pop() to return a result from a pushed screen.
- Use
IndexedStack when preserving the state of tab widgets is important.
- For complex nested navigation or deep linking, consider Router-based navigation or a routing package such as
go_router.
- Avoid unnecessarily duplicating the same state across multiple bottom-navigation screens.
31. Common Mistakes
Mistake 1: Creating Separate Copies of Shared Data
If Home, Cart, and Profile each maintain separate copies of the same data, the screens can become inconsistent.
Instead, keep shared state in a common owner or use an appropriate state-management approach.
Mistake 2: Confusing Tab Switching With Route Navigation
Changing the selected NavigationBar index is different from pushing a new route with Navigator.push().
Mistake 3: Using Global Variables Unnecessarily
Global variables can make data flow difficult to trace. Prefer explicit data flow or a suitable state-management architecture.
Mistake 4: Passing Too Many Parameters
If a screen requires many related values, create a model object instead of passing many independent parameters.
Mistake 5: Ignoring Tab State
If each tab contains forms, scroll positions, filters, or other local state, consider whether the selected navigation architecture should preserve those states.
32. Bottom Navigation Data Flow
MainScreen
|
Shared Application State
|
+-------------+-------------+
| | |
Home Products Cart
| | |
+-------------+-------------+
|
NavigationBar
|
selectedIndex / destinations
The important concept is that the bottom-navigation destinations do not necessarily need to communicate directly with each other. A common parent or shared state layer can coordinate the information.
33. Interview Questions
Q1. What is NavigationBar in Flutter?
NavigationBar is a Material 3 widget used for persistent navigation between primary destinations in an application.
Q2. What is the difference between NavigationBar and BottomNavigationBar?
NavigationBar is the Material 3 component, while BottomNavigationBar is the older Material 2 component.
Q3. How can you send information from one bottom-navigation tab to another?
For a small application, the common parent can own the shared state and pass it to the required tabs through constructors. Callbacks can be used to send updates back to the parent.
Q4. Can Navigator.push() be used inside a bottom-navigation tab?
Yes. A bottom-navigation destination can use Navigator.push() to open a detail, edit, or other temporary route.
Q5. How do you return information from a detail screen?
Use Navigator.pop(context, result) and await the result from the pushed route.
Q6. When should state management be used?
State management becomes useful when multiple unrelated widgets or destinations need to read and update the same application state.
Q7. What is IndexedStack used for?
IndexedStack can display one child at a time while keeping the other children in the widget tree, which can help preserve the state of bottom-navigation tabs.
34. Practice Exercises
- Create a Flutter app with Home, Products, Cart, and Profile tabs.
- Create a Product model and send selected products from Home to Cart.
- Add a cart counter badge to the Cart navigation destination.
- Create a Profile model and make it available to Home and Profile tabs.
- Create a Products tab and open a Product Details route using
Navigator.push().
- Return a selected product from the Product Details screen using
Navigator.pop().
- Use
IndexedStack to preserve the state of multiple tabs.
- Build the same application using a state-management approach.
- Add a search field to Home and pass the search query to the Products tab.
- Implement separate navigation stacks for two bottom-navigation destinations.
35. Quick Revision
Concept |
Syntax / Approach |
|---|
Modern Material bottom navigation |
NavigationBar |
Older Material bottom navigation |
BottomNavigationBar |
Track selected destination |
selectedIndex |
Change selected destination |
onDestinationSelected |
Send data to child |
Constructor parameter |
Send data to parent |
Callback |
Share state between tabs |
Common parent/state-management solution |
Open detail screen |
Navigator.push() |
Return information |
Navigator.pop(context, result) |
Preserve tab widget state |
IndexedStack |
Complex navigation |
Router / go_router |
36. Key Takeaways
- Flutter's
NavigationBar provides Material 3 bottom navigation.
- Bottom navigation is primarily used for switching between an app's main destinations.
- Data can be shared between tabs through a common parent.
- Constructor parameters provide a clear way to pass information to a destination.
- Callbacks allow a child screen to notify the parent about changes.
- Model classes are useful for passing structured information.
Navigator.push() can open a detail route from a bottom-navigation tab.
Navigator.pop() can return information from that detail route.
IndexedStack can help preserve the state of tab widgets.
- State-management solutions become useful when shared data grows across the application.
- Router-based navigation or packages such as
go_router can be considered for advanced navigation and deep-linking requirements.
37. Official Flutter Resources
38. Flutter Training Resources