In Flutter, navigation is a crucial aspect of building complex applications. As apps grow in size and complexity, the need for nested navigation arises. Nested navigation refers to having navigation stacks within navigation stacks, allowing users to navigate different sections or modules of an app independently. In this comprehensive guide, we’ll explore how to handle nested navigation effectively in Flutter.
What is Nested Navigation?
Nested navigation involves having separate navigation stacks within an application. For instance, consider an e-commerce app where the main navigation bar has tabs for “Home,” “Shop,” “Cart,” and “Profile.” Each tab might have its own stack of screens, and navigating within one tab should not affect the navigation state of the others. This is where nested navigation becomes essential.
Why Use Nested Navigation?
- Modularity: Keeps different sections of your app independent.
- State Preservation: Preserves the navigation state within each section when switching between tabs or modules.
- User Experience: Provides a more intuitive and seamless user experience, especially in complex apps.
Approaches to Handling Nested Navigation in Flutter
There are several approaches to handle nested navigation in Flutter:
1. Using PageView and Navigator Widgets
One of the simplest approaches is to use the PageView widget in combination with multiple Navigator widgets. Each page in the PageView has its own Navigator, creating separate navigation stacks.
Step 1: Set Up a Basic Tab Structure
First, create a basic tab structure using a BottomNavigationBar and PageView.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Nested Navigation Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State {
int _currentIndex = 0;
final List _pages = [
HomeScreen(),
ShopScreen(),
CartScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Nested Navigation')),
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'Cart'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
Step 2: Implement Individual Pages with Navigators
Each page will have its own Navigator to maintain its stack. Here’s how you can implement the HomeScreen:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Navigator(
key: GlobalKey(),
onGenerateRoute: (settings) {
return MaterialPageRoute(
builder: (context) => HomePage(),
);
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Home Screen'),
ElevatedButton(
child: Text('Go to Details'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => DetailsScreen(title: 'Home Details')),
);
},
),
],
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
final String title;
DetailsScreen({required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('Details Screen'),
),
);
}
}
Implement similar Navigator setups for ShopScreen, CartScreen, and ProfileScreen.
Step 3: Maintaining the PageView with IndexedStack
A PageView can cause screens to rebuild when switching tabs, which can be undesirable. To prevent this, use IndexedStack to preserve the state of each page:
class _MainScreenState extends State {
int _currentIndex = 0;
final List _pages = [
HomeScreen(),
ShopScreen(),
CartScreen(),
ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Nested Navigation')),
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'Cart'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
2. Using the go_router Package
The go_router package provides a declarative way to define routes, including nested routes. It simplifies navigation and makes it more maintainable.
Step 1: Add the go_router Dependency
Add go_router to your pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
go_router: ^13.2.0
Run flutter pub get to install the package.
Step 2: Define Routes with GoRouter
Configure the GoRouter with your routes. Define a nested structure for each main section.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
title: 'GoRouter Nested Navigation',
);
}
}
final GoRouter _router = GoRouter(
initialLocation: '/home',
routes: [
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) => MainScreen(),
),
],
);
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State {
int _currentIndex = 0;
final List _tabPaths = ['/home/home', '/shop', '/cart', '/profile'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Nested Navigation')),
body: IndexedStack(
index: _currentIndex,
children: [
Navigator(
key: GlobalKey(),
onGenerateRoute: (settings) {
Widget page;
switch (settings.name) {
case '/home/home':
page = HomeScreen();
break;
case '/home/details':
page = DetailsScreen(title: 'Home Details');
break;
default:
page = Scaffold(body: Center(child: Text('Unknown route')));
}
return MaterialPageRoute(builder: (context) => page);
},
),
Navigator(
key: GlobalKey(),
onGenerateRoute: (settings) {
Widget page;
switch (settings.name) {
case '/shop':
page = ShopScreen();
break;
case '/shop/details':
page = ShopDetailsScreen(title: 'Shop Details');
break;
default:
page = Scaffold(body: Center(child: Text('Unknown route')));
}
return MaterialPageRoute(builder: (context) => page);
},
),
CartScreen(),
ProfileScreen(),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
// Use Navigator.pushNamed to navigate within the current Navigator
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'Cart'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Home Screen'),
ElevatedButton(
child: Text('Go to Details'),
onPressed: () {
Navigator.of(context).pushNamed('/home/details');
},
),
],
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
final String title;
DetailsScreen({required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('Details Screen'),
),
);
}
}
class ShopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Shop')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Shop Screen'),
ElevatedButton(
child: Text('Go to Shop Details'),
onPressed: () {
Navigator.of(context).pushNamed('/shop/details');
},
),
],
),
),
);
}
}
class ShopDetailsScreen extends StatelessWidget {
final String title;
ShopDetailsScreen({required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('Shop Details Screen'),
),
);
}
}
class CartScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Cart')),
body: Center(
child: Text('Cart Screen'),
),
);
}
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Center(
child: Text('Profile Screen'),
),
);
}
}
3. Using a Custom Navigator Solution
For advanced use cases, you might consider building a custom navigator solution. This approach gives you complete control over the navigation stack and behavior. However, it also requires more effort and a deeper understanding of Flutter’s navigation system.
Step 1: Create a Custom Navigation Manager
Build a class to manage navigation stacks for different sections.
class CustomNavigationManager {
final Map>> _navigationStacks = {};
void push(String section, Route route) {
if (!_navigationStacks.containsKey(section)) {
_navigationStacks[section] = [];
}
_navigationStacks[section]!.add(route);
}
Route? pop(String section) {
if (!_navigationStacks.containsKey(section) || _navigationStacks[section]!.isEmpty) {
return null;
}
return _navigationStacks[section]!.removeLast();
}
List> getStack(String section) {
return _navigationStacks[section] ?? [];
}
}
Step 2: Integrate the Custom Navigation Manager
In your main screen, use the CustomNavigationManager to handle navigation for each section.
class MainScreen extends StatefulWidget {
@override
_MainScreenState createState() => _MainScreenState();
}
class _MainScreenState extends State {
int _currentIndex = 0;
final CustomNavigationManager _navigationManager = CustomNavigationManager();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Nested Navigation')),
body: Navigator(
onGenerateRoute: (settings) {
Widget page;
switch (settings.name) {
case '/home':
page = HomeScreen(navigationManager: _navigationManager);
break;
case '/shop':
page = ShopScreen(navigationManager: _navigationManager);
break;
case '/cart':
page = CartScreen();
break;
case '/profile':
page = ProfileScreen();
break;
default:
page = Center(child: Text('Unknown route'));
}
return MaterialPageRoute(builder: (context) => page);
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.shop), label: 'Shop'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: 'Cart'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
class HomeScreen extends StatelessWidget {
final CustomNavigationManager navigationManager;
HomeScreen({required this.navigationManager});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Home Screen'),
ElevatedButton(
child: Text('Go to Details'),
onPressed: () {
navigationManager.push(
'home',
MaterialPageRoute(builder: (context) => DetailsScreen(title: 'Home Details')),
);
},
),
],
),
),
);
}
}
Conclusion
Handling nested navigation in Flutter is crucial for building complex, modular applications. By leveraging techniques like PageView with Navigator, packages like go_router, or custom navigation solutions, you can create an intuitive and seamless user experience. Each approach has its benefits and trade-offs, so choose the one that best fits the needs of your project. Understanding and implementing nested navigation effectively will greatly enhance the maintainability and scalability of your Flutter apps.