Flutter Navigation 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
- Imperative Navigation
- Named Routes
- Advanced Navigation Patterns
- Navigation with Data
- Navigation Best Practices
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
Navigatorclass - 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)