Table of Contents
Introduction to Navigation in Flutter
Basic Navigation with Navigator.push and Navigator.pop
2.1 Understanding the Navigator Widget
2.2 Implementing Navigator.push
2.3 Using Navigator.pop
2.4 Real-Life Example: A Shopping App Screen Transition
Named Routes and Route Management
3.1 Defining Named Routes
3.2 Navigating with Named Routes
3.3 Managing Routes with MaterialApp
3.4 Real-Life Example: A Social Media App
Passing Data Between Screens
4.1 Passing Data with Navigator.push
4.2 Returning Data with Navigator.pop
4.3 Real-Life Example: A Note-Taking App
Advanced Routing with go_router
5.1 Setting Up go_router
5.2 Configuring Routes and Sub-Routes
5.3 Handling Query Parameters
5.4 Real-Life Example: An E-Commerce App
Handling Deep Links in Flutter Apps
6.1 Understanding Deep Links
6.2 Configuring Deep Links
6.3 Handling Deep Links with go_router
6.4 Real-Life Example: A News App
Practical Exercise: Building a To-Do List App
7.1 Project Setup
7.2 Creating the List View Screen
7.3 Creating the Detail View Screen
7.4 Implementing Navigation and Data Passing
7.5 Adding Deep Link Support
Best Practices for Navigation in Flutter
Pros and Cons of Navigation Approaches
Alternatives to go_router
Exception Handling in Navigation
Conclusion
1. Introduction to Navigation in Flutter
Navigation is the backbone of any multi-screen mobile application. In Flutter, navigation allows users to move seamlessly between screens, whether it's to view details, submit forms, or access settings. This module focuses on mastering Flutter's navigation and routing techniques, from basic screen transitions to advanced routing with packages like go_router and handling deep links for real-world apps.
By the end of this module, you'll be able to:
Create multi-screen apps with smooth transitions.
Pass and retrieve data between screens.
Implement advanced routing with go_router.
Handle deep links to enhance user experience.
We'll use a practical to-do list app as a hands-on example, inspired by real-life scenarios like task management apps used daily.
2. Basic Navigation with Navigator.push and Navigator.pop
2.1 Understanding the Navigator Widget
Flutter's Navigator widget manages a stack of routes, where each route represents a screen. The stack operates on a "last in, first out" principle, meaning the most recently pushed screen is displayed, and popping removes it to reveal the previous screen.
2.2 Implementing Navigator.push
Navigator.push adds a new route to the stack, transitioning to a new screen. Here's a basic example:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: FirstScreen(),
);
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Text('Go to Second Screen'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(child: Text('Welcome to the Second Screen!')),
);
}
}
Explanation:
Navigator.push takes a context and a Route (e.g., MaterialPageRoute).
MaterialPageRoute builds the new screen (SecondScreen) and animates the transition.
2.3 Using Navigator.pop
Navigator.pop removes the topmost screen, returning to the previous one:
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
),
);
}
}
Explanation:
Navigator.pop(context) removes SecondScreen from the stack, returning to FirstScreen.
2.4 Real-Life Example: A Shopping App Screen Transition
Imagine a shopping app where users browse products on the home screen and tap a product to view its details. Here's how you can implement this:
import 'package:flutter/material.dart';
void main() {
runApp(ShoppingApp());
}
class ShoppingApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ProductListScreen(),
);
}
}
class ProductListScreen extends StatelessWidget {
final List<String> products = ['Laptop', 'Phone', 'Tablet'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product List')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index]),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(product: products[index]),
),
);
},
);
},
),
);
}
}
class ProductDetailScreen extends StatelessWidget {
final String product;
ProductDetailScreen({required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('$product Details')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Details for $product'),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Back to List'),
),
],
),
),
);
}
}
Explanation:
The ProductListScreen displays a list of products.
Tapping a product uses Navigator.push to show ProductDetailScreen.
Navigator.pop returns to the product list.
Best Practices:
Use MaterialPageRoute for standard transitions.
Ensure the context is valid when calling Navigator methods.
Avoid excessive nesting of screens to prevent stack overflow.
3. Named Routes and Route Management
3.1 Defining Named Routes
Named routes simplify navigation by assigning string identifiers to screens. Define them in MaterialApp:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailsScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/details');
},
child: Text('Go to Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
),
);
}
}
Explanation:
MaterialApp.routes maps route names to screen widgets.
Navigator.pushNamed navigates to the specified route.
3.2 Navigating with Named Routes
Use Navigator.pushNamed and Navigator.pop for navigation:
Navigator.pushNamed(context, '/details');
// To go back
Navigator.pop(context);
3.3 Managing Routes with MaterialApp
For dynamic routes, use onGenerateRoute:
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/details') {
return MaterialPageRoute(
builder: (context) => DetailsScreen(),
);
}
return null; // Handle unknown routes
},
)
3.4 Real-Life Example: A Social Media App
In a social media app, users navigate from a feed to a profile screen using named routes:
import 'package:flutter/material.dart';
void main() {
runApp(SocialMediaApp());
}
class SocialMediaApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/feed',
routes: {
'/feed': (context) => FeedScreen(),
'/profile': (context) => ProfileScreen(),
},
);
}
}
class FeedScreen extends StatelessWidget {
final List<String> posts = ['Post 1', 'Post 2', 'Post 3'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Feed')),
body: ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(posts[index]),
onTap: () {
Navigator.pushNamed(context, '/profile');
},
);
},
),
);
}
}
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Back to Feed'),
),
),
);
}
}
Explanation:
Routes are defined in MaterialApp.
Tapping a post navigates to the profile screen using Navigator.pushNamed.
Best Practices:
Use descriptive route names (e.g., /profile instead of /p).
Centralize route definitions for maintainability.
Handle unknown routes to avoid crashes.
4. Passing Data Between Screens
4.1 Passing Data with Navigator.push
Pass data by including it in the screen's constructor:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsScreen(data: 'Some Data'),
),
);
4.2 Returning Data with Navigator.pop
Return data to the previous screen using Navigator.pop:
Navigator.pop(context, 'Result Data');
Receive the result in the calling screen:
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailsScreen()),
);
4.3 Real-Life Example: A Note-Taking App
In a note-taking app, users create a note and return it to the main screen:
import 'package:flutter/material.dart';
void main() {
runApp(NoteApp());
}
class NoteApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: NoteListScreen(),
);
}
}
class NoteListScreen extends StatelessWidget {
final List<String> notes = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notes')),
body: ListView.builder(
itemCount: notes.length,
itemBuilder: (context, index) {
return ListTile(title: Text(notes[index]));
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newNote = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddNoteScreen()),
);
if (newNote != null) {
notes.add(newNote);
}
},
child: Icon(Icons.add),
),
);
}
}
class AddNoteScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Note')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Note'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
Navigator.pop(context, _controller.text);
},
child: Text('Save Note'),
),
],
),
),
);
}
}
Explanation:
NoteListScreen navigates to AddNoteScreen.
AddNoteScreen returns the note text using Navigator.pop.
The note is added to the list if not null.
Best Practices:
Validate data before passing to avoid null errors.
Use immutable data models for complex objects.
Handle null returns gracefully.
5. Advanced Routing with go_router
5.1 Setting Up go_router
go_router is a powerful package for advanced routing. Add it to pubspec.yaml:
dependencies:
go_router: ^10.0.0
5.2 Configuring Routes and Sub-Routes
Configure routes in MaterialApp.router:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(MyApp());
}
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/details',
builder: (context, state) => DetailsScreen(),
),
],
);
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/details');
},
child: Text('Go to Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/');
},
child: Text('Go Back'),
),
),
);
}
}
5.3 Handling Query Parameters
Pass data via query parameters:
context.go('/details?item=Phone');
Retrieve in DetailsScreen:
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final item = GoRouterState.of(context).queryParameters['item'] ?? 'Unknown';
return Scaffold(
appBar: AppBar(title: Text('Details')),
body: Center(child: Text('Item: $item')),
);
}
}
5.4 Real-Life Example: An E-Commerce App
An e-commerce app with product listing and details:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(ECommerceApp());
}
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => ProductListScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductDetailScreen(id: id);
},
),
],
);
class ECommerceApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
class ProductListScreen extends StatelessWidget {
final List<Map<String, String>> products = [
{'id': '1', 'name': 'Laptop'},
{'id': '2', 'name': 'Phone'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(products[index]['name']!),
onTap: () {
context.go('/product/${products[index]['id']}');
},
);
},
),
);
}
}
class ProductDetailScreen extends StatelessWidget {
final String id;
ProductDetailScreen({required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product $id')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/');
},
child: Text('Back to Products'),
),
),
);
}
}
Explanation:
go_router handles dynamic routes with path parameters (/product/:id).
context.go navigates to the product details screen.
Best Practices:
Use go_router for large apps with complex routing.
Leverage path and query parameters for flexibility.
Keep route configurations modular.
6. Handling Deep Links in Flutter Apps
6.1 Understanding Deep Links
Deep links allow users to navigate directly to specific app content from external sources (e.g., a web link). For example, clicking a news article link opens the app's article screen.
6.2 Configuring Deep Links
Add deep link support using go_router:
dependencies:
go_router: ^10.0.0
uni_links: ^0.5.1
Configure in android/app/src/main/AndroidManifest.xml:
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="example.com"/>
</intent-filter>
</activity>
For iOS, update ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
6.3 Handling Deep Links with go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
void main() {
runApp(NewsApp());
}
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomeScreen(),
),
GoRoute(
path: '/article/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ArticleScreen(id: id);
},
),
],
redirect: (context, state) async {
final uri = await getInitialLink();
if (uri != null && uri.startsWith('myapp://example.com/article/')) {
final id = uri.split('/').last;
return '/article/$id';
}
return null;
},
);
class NewsApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('News Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
context.go('/article/123');
},
child: Text('View Article'),
),
),
);
}
}
class ArticleScreen extends StatelessWidget {
final String id;
ArticleScreen({required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Article $id')),
body: Center(child: Text('Reading Article $id')),
);
}
}
6.4 Real-Life Example: A News App
A deep link like myapp://example.com/article/123 opens the article directly:
// Add to main.dart
StreamSubscription? _sub;
void initUniLinks() {
_sub = linkStream.listen((String? link) {
if (link != null && link.startsWith('myapp://example.com/article/')) {
final id = link.split('/').last;
_router.go('/article/$id');
}
}, onError: (err) {
// Handle error
});
}
Explanation:
uni_links listens for deep links.
go_router redirects to the appropriate screen.
Best Practices:
Test deep links thoroughly on both platforms.
Handle invalid links gracefully.
Secure deep links to prevent unauthorized access.
7. Practical Exercise: Building a To-Do List App
7.1 Project Setup
Create a new Flutter project and add dependencies:
dependencies:
flutter:
sdk: flutter
go_router: ^10.0.0
7.2 Creating the List View Screen
The list view displays tasks:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(TodoApp());
}
final GoRouter _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => TodoListScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
final title = state.queryParameters['title'] ?? '';
return TodoDetailScreen(id: id, title: title);
},
),
],
);
class TodoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router,
);
}
}
class TodoListScreen extends StatefulWidget {
@override
_TodoListScreenState createState() => _TodoListScreenState();
}
class _TodoListScreenState extends State<TodoListScreen> {
final List<Map<String, String>> todos = [
{'id': '1', 'title': 'Buy groceries'},
{'id': '2', 'title': 'Finish homework'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('To-Do List')),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index]['title']!),
onTap: () {
context.go('/details/${todos[index]['id']}?title=${todos[index]['title']}');
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => AddTodoScreen()),
);
if (result != null) {
setState(() {
todos.add({'id': '${todos.length + 1}', 'title': result});
});
}
},
child: Icon(Icons.add),
),
);
}
}
7.3 Creating the Detail View Screen
The detail view shows task details:
class TodoDetailScreen extends StatelessWidget {
final String id;
final String title;
TodoDetailScreen({required this.id, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Task Details')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Task ID: $id'),
Text('Title: $title'),
ElevatedButton(
onPressed: () {
context.go('/');
},
child: Text('Back to List'),
),
],
),
),
);
}
}
7.4 Implementing Navigation and Data Passing
Add a screen to create new tasks:
class AddTodoScreen extends StatelessWidget {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Add Task')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(labelText: 'Task Title'),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_controller.text.isNotEmpty) {
Navigator.pop(context, _controller.text);
}
},
child: Text('Save Task'),
),
],
),
),
);
}
}
7.5 Adding Deep Link Support
Add deep link support for tasks (e.g., myapp://todo.com/details/1):
import 'package:uni_links/uni_links.dart';
import 'dart:async';
// Add to TodoApp
class TodoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
initUniLinks();
return MaterialApp.router(
routerConfig: _router,
);
}
void initUniLinks() {
linkStream.listen((String? link) {
if (link != null && link.startsWith('myapp://todo.com/details/')) {
final id = link.split('/').last;
_router.go('/details/$id');
}
}, onError: (err) {
// Handle error
});
}
}
Explanation:
The app uses go_router for navigation.
Tasks are displayed in a list, and tapping navigates to the detail screen.
Deep links open specific tasks directly.
8. Best Practices for Navigation in Flutter
Use Named Routes for Scalability: Centralize route definitions for large apps.
Validate Data: Ensure passed data is valid to avoid null errors.
Handle Errors: Implement error handling for invalid routes or deep links.
Optimize Stack Management: Avoid excessive screen stacking.
Test Transitions: Ensure smooth animations across platforms.
9. Pros and Cons of Navigation Approaches
Navigator.push/pop:
Pros: Simple, built-in, no dependencies.
Cons: Limited for complex apps, manual stack management.
Named Routes:
Pros: Organized, reusable, easy to maintain.
Cons: Requires upfront route definitions.
go_router:
Pros: Advanced features, deep link support, parameter handling.
Cons: Dependency overhead, steeper learning curve.
10. Alternatives to go_router
auto_route: Offers type-safe routing and code generation.
fluro: Lightweight, supports query parameters and wildcards.
Navigator 2.0: Flutter's advanced API for custom routing logic.
11. Exception Handling in Navigation
Handle navigation errors to prevent crashes:
try {
Navigator.pushNamed(context, '/invalid-route');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Route not found!')),
);
}
For go_router, use errorBuilder:
final GoRouter _router = GoRouter(
routes: [...],
errorBuilder: (context, state) => ErrorScreen(error: state.error.toString()),
);
12. Conclusion
This module covered Flutter navigation from basic Navigator.push/pop to advanced go_router and deep links. By building a to-do list app, you learned practical, real-world navigation techniques. Apply these skills to create seamless, user-friendly multi-screen apps.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam