Flutter Navigation Complete Guide
A comprehensive guide to Flutter navigation patterns with detailed explanations, use cases, and practical examples. This guide covers both imperative and declarative navigation approaches.
Table of Contents
Navigation Fundamentals
Understanding Navigation in Flutter
What is Navigation? Navigation in Flutter refers to the process of moving between different screens (pages) in your app. Flutter provides a stack-based navigation system where screens are pushed onto and popped off a navigation stack.
Key Concepts:
- Navigation Stack: A stack of screens where the top screen is currently visible
- Push: Add a new screen to the top of the stack
- Pop: Remove the current screen from the top of the stack
- Route: A screen or page in your app
Navigation Approaches
Imperative Navigation:
- Direct control over navigation stack
- Programmatic navigation using
Navigator
class - Good for complex navigation logic
Declarative Navigation:
- Route-based navigation using named routes
- Centralized route definitions
- Better for deep linking and web URLs
Imperative Navigation
Basic Navigation Operations
Push (Navigate Forward)
What it does: Adds a new screen to the navigation stack.
When to use:
- Moving to a detail screen
- Opening forms or modals
- Any forward navigation where you want to return
// Basic push navigation
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailsPage(),
),
);
},
child: const Text('Open Details'),
)
// Push with custom route settings
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailsPage(),
settings: RouteSettings(name: '/details'),
fullscreenDialog: true, // Shows as modal on iOS
),
)
Pop (Navigate Back)
What it does: Removes the current screen from the navigation stack.
When to use:
- Returning to the previous screen
- Closing modals or dialogs
- Canceling operations
// Basic pop navigation
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Back'),
)
// Pop with data (returning data to previous screen)
ElevatedButton(
onPressed: () => Navigator.of(context).pop('Selected Item'),
child: const Text('Select and Return'),
)
// Pop with multiple screens (go back multiple levels)
ElevatedButton(
onPressed: () => Navigator.of(context).popUntil((route) => route.isFirst),
child: const Text('Go to Home'),
)
Advanced Navigation Operations
PushReplacement (Replace Current Screen)
What it does: Replaces the current screen with a new one, removing the current screen from the stack.
When to use:
- Login/logout flows
- Splash screen to main app
- When you don't want users to go back to the previous screen
// Replace current screen
ElevatedButton(
onPressed: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomePage(),
),
);
},
child: const Text('Go to Home'),
)
// Replace with custom settings
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomePage(),
settings: RouteSettings(name: '/home'),
),
)
PushAndRemoveUntil (Clear Stack)
What it does: Pushes a new screen and removes all previous screens from the stack based on a condition.
When to use:
- After successful login (clear login screens)
- App initialization flows
- When you want to prevent back navigation to certain screens
// Clear entire stack and go to home
ElevatedButton(
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => const HomePage(),
),
(route) => false, // Remove all routes
);
},
child: const Text('Login and Go Home'),
)
// Clear stack until a specific route
ElevatedButton(
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (context) => const ProfilePage(),
),
(route) => route.settings.name == '/home', // Keep home route
);
},
child: const Text('Go to Profile'),
)
PopUntil (Go Back to Specific Screen)
What it does: Pops screens from the stack until a specific condition is met.
When to use:
- Going back to a specific screen in the stack
- Clearing intermediate screens
- Navigation breadcrumbs
// Go back to home screen
ElevatedButton(
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
child: const Text('Go to Home'),
)
// Go back to specific named route
ElevatedButton(
onPressed: () {
Navigator.of(context).popUntil((route) => route.settings.name == '/profile');
},
child: const Text('Go to Profile'),
)
Named Routes
Setting Up Named Routes
What it does: Defines routes by name for centralized navigation management.
When to use:
- Large apps with many screens
- Deep linking support
- Web URL routing
- Consistent navigation patterns
// Define routes in MaterialApp
MaterialApp(
title: 'My App',
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/login': (context) => const LoginPage(),
'/profile': (context) => const ProfilePage(),
'/details': (context) => const DetailsPage(),
'/settings': (context) => const SettingsPage(),
},
// Optional: Handle unknown routes
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (context) => const NotFoundPage(),
);
},
)
Navigation with Named Routes
Basic Named Route Navigation
// Navigate to named route
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/details'),
child: const Text('Go to Details'),
)
// Navigate and replace current screen
ElevatedButton(
onPressed: () => Navigator.pushReplacementNamed(context, '/home'),
child: const Text('Go to Home'),
)
// Navigate and clear stack
ElevatedButton(
onPressed: () => Navigator.pushNamedAndRemoveUntil(
context,
'/home',
(route) => false,
),
child: const Text('Login and Go Home'),
)
Named Routes with Arguments
// Define route with arguments
MaterialApp(
routes: {
'/': (context) => const HomePage(),
'/details': (context) => const DetailsPage(),
},
// Handle routes with arguments
onGenerateRoute: (settings) {
if (settings.name == '/details') {
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (context) => DetailsPage(
id: args['id'],
title: args['title'],
),
);
}
return null;
},
)
// Navigate with arguments
ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
'/details',
arguments: {
'id': 123,
'title': 'Product Details',
},
);
},
child: const Text('View Details'),
)
// Access arguments in the destination screen
class DetailsPage extends StatelessWidget {
final int id;
final String title;
const DetailsPage({
Key? key,
required this.id,
required this.title,
}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('Details for item $id'),
),
);
}
}
Advanced Navigation Patterns
Modal Navigation
Full-Screen Dialog
What it does: Presents a screen as a modal dialog.
When to use:
- Login screens
- Important forms
- Confirmation dialogs
// Present as full-screen dialog
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const LoginPage(),
fullscreenDialog: true, // Shows as modal on iOS
),
)
Bottom Sheet
What it does: Slides up from the bottom of the screen.
When to use:
- Quick actions
- Selection menus
- Additional options
// Show bottom sheet
ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => Container(
height: 200,
child: Column(
children: [
ListTile(
leading: Icon(Icons.edit),
title: Text('Edit'),
onTap: () {
Navigator.pop(context);
// Handle edit action
},
),
ListTile(
leading: Icon(Icons.delete),
title: Text('Delete'),
onTap: () {
Navigator.pop(context);
// Handle delete action
},
),
],
),
),
);
},
child: const Text('Show Options'),
)
Tab Navigation
TabBar with TabBarView
class TabNavigationPage extends StatefulWidget {
_TabNavigationPageState createState() => _TabNavigationPageState();
}
class _TabNavigationPageState extends State<TabNavigationPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Tab Navigation'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(icon: Icon(Icons.home), text: 'Home'),
Tab(icon: Icon(Icons.search), text: 'Search'),
Tab(icon: Icon(Icons.person), text: 'Profile'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
HomeTab(),
SearchTab(),
ProfileTab(),
],
),
);
}
void dispose() {
_tabController.dispose();
super.dispose();
}
}
Navigation with Data
Passing Data Forward
Constructor Parameters
// Pass data through constructor
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsPage(
itemId: 123,
itemTitle: 'Product Name',
),
),
);
},
child: const Text('View Details'),
)
// Receive data in destination screen
class DetailsPage extends StatelessWidget {
final int itemId;
final String itemTitle;
const DetailsPage({
Key? key,
required this.itemId,
required this.itemTitle,
}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(itemTitle)),
body: Center(
child: Text('Details for item $itemId'),
),
);
}
}
Returning Data Back
Pop with Data
// Return data when popping
ElevatedButton(
onPressed: () {
Navigator.pop(context, 'Selected Option');
},
child: const Text('Select Option'),
)
// Receive returned data
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SelectionPage(),
),
);
if (result != null) {
print('Selected: $result');
// Handle the returned data
}
},
child: const Text('Open Selection'),
)
Awaiting Navigation Results
// Wait for navigation result
Future<void> _showConfirmationDialog() async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Confirm Action'),
content: Text('Are you sure you want to proceed?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Confirm'),
),
],
),
);
if (result == true) {
// User confirmed the action
_performAction();
}
}
Navigation Best Practices
Performance Optimization
Lazy Loading
// Use lazy loading for heavy screens
MaterialApp(
routes: {
'/': (context) => const HomePage(),
'/heavy-screen': (context) => const HeavyScreen(),
},
onGenerateRoute: (settings) {
// Lazy load heavy screens
if (settings.name == '/heavy-screen') {
return MaterialPageRoute(
builder: (context) => FutureBuilder(
future: Future.delayed(Duration(milliseconds: 100)),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return const HeavyScreen();
}
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
},
),
);
}
return null;
},
)
Route Guards
// Implement route guards for authentication
MaterialApp(
onGenerateRoute: (settings) {
// Check authentication for protected routes
if (_isProtectedRoute(settings.name) && !_isAuthenticated()) {
return MaterialPageRoute(
builder: (context) => const LoginPage(),
);
}
// Return normal route
return _getRoute(settings);
},
)
bool _isProtectedRoute(String? routeName) {
return ['/profile', '/settings', '/admin'].contains(routeName);
}
bool _isAuthenticated() {
// Check authentication status
return authService.isLoggedIn;
}
Error Handling
Handle Navigation Errors
// Safe navigation with error handling
Future<void> _safeNavigate(String routeName) async {
try {
await Navigator.pushNamed(context, routeName);
} catch (e) {
// Handle navigation errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Navigation failed: $e'),
backgroundColor: Colors.red,
),
);
}
}
Navigation State Management
Track Navigation State
class NavigationService {
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static NavigatorState get navigator => navigatorKey.currentState!;
static Future<dynamic> pushNamed(String routeName, {Object? arguments}) {
return navigator.pushNamed(routeName, arguments: arguments);
}
static void pop([dynamic result]) {
return navigator.pop(result);
}
}
// Use in MaterialApp
MaterialApp(
navigatorKey: NavigationService.navigatorKey,
routes: {
'/': (context) => const HomePage(),
'/details': (context) => const DetailsPage(),
},
)
// Use anywhere in the app
ElevatedButton(
onPressed: () => NavigationService.pushNamed('/details'),
child: const Text('Go to Details'),
)
References
- Flutter Navigation (flutter.dev)
- Navigator API (api.flutter.dev)
- Material Design Navigation (material.io)
- Flutter Deep Linking (flutter.dev)