Navigation and Data Transfer Between Pages in Flutter
Page Navigation - Using Navigator
The first time I tried to navigate between pages, I was a bit confused. "The web has links, how does Flutter do it?" I wondered. Then I discovered Navigator and everything became clear.
In Flutter, the Navigator class is used for page navigation. It works like a stack data structure: You open a page, it's added to the top; you press back, the top page is removed.
Live Demo: Using Navigator
Try page navigation and data transfer interactively:
Basic Navigator Usage
The simplest way to navigate from one page to another:
// Home page
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Page')),
body: Center(
child: ElevatedButton(
child: Text('Go to Detail Page'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);
},
),
),
);
}
}
// Detail page
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail Page')),
body: Center(
child: Text('This is the detail page'),
),
);
}
}Navigator.push() opens a new page. MaterialPageRoute provides the page transition animation.
Going Back - pop()
There are two ways to go back:
1. Automatic Back Button
If you use AppBar, Flutter automatically adds a back button. The user clicks it to go back.
2. Programmatic Back Navigation
ElevatedButton(
child: Text('Go Back'),
onPressed: () {
Navigator.pop(context);
},
)Navigator.pop() removes the top page from the stack.
Sending Data Between Pages
This is where it gets interesting. We use constructors to send data to pages.
Page Sending Data
class ProductListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Products')),
body: ListView(
children: [
ListTile(
title: Text('iPhone 15'),
subtitle: Text('\$999'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'iPhone 15',
price: 999,
description: 'Next generation smartphone',
),
),
);
},
),
ListTile(
title: Text('Samsung S24'),
subtitle: Text('\$899'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
productName: 'Samsung S24',
price: 899,
description: 'The best of Android',
),
),
);
},
),
],
),
);
}
}Page Receiving Data
class ProductDetailPage extends StatelessWidget {
final String productName;
final int price;
final String description;
const ProductDetailPage({
Key? key,
required this.productName,
required this.price,
required this.description,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(productName)),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
productName,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'\$${price.toString()}',
style: TextStyle(fontSize: 20, color: Colors.green),
),
SizedBox(height: 16),
Text(
description,
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
}We use required in the constructor to make parameters mandatory. This way, when opening the page, this information must be provided.
Returning Data When Going Back
Sometimes you want to receive data from the page you opened. For example, the user selected something and you want to send this selection to the previous page.
Page Requesting Data
class SelectCityPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Select City')),
body: Center(
child: ElevatedButton(
child: Text('Choose City'),
onPressed: () async {
// Open page and wait for result
final selectedCity = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => CityListPage()),
);
// Result received
if (selectedCity != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Selected: $selectedCity')),
);
}
},
),
),
);
}
}Page Returning Data
class CityListPage extends StatelessWidget {
final List<String> cities = [
'New York',
'Los Angeles',
'Chicago',
'Houston',
'Phoenix',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('City List')),
body: ListView.builder(
itemCount: cities.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(cities[index]),
onTap: () {
// Send selected city back
Navigator.pop(context, cities[index]);
},
);
},
),
);
}
}With Navigator.pop(context, value), we send data as the second parameter.
pushReplacement - Preventing Back Navigation
Sometimes you don't want the user to go back to the previous page using the back button. A typical example: Don't return to the login page after logging in.
Normal Navigation with push()
Navigator.push(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
// User can press back to return to login pageNavigation with pushReplacement()
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
// Back button goes to the page before login, skipping loginPractical Example: After Login
class LoginPage extends StatelessWidget {
void _login(BuildContext context) {
// Login successful
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
child: Text('Login'),
onPressed: () => _login(context),
),
),
);
}
}Now when pressing back on HomePage, it won't return to LoginPage, but will exit the app.
After Registration Example
class RegisterPage extends StatelessWidget {
void _register(BuildContext context) {
// Registration successful
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Registration successful! You can now login.')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Register')),
body: Center(
child: ElevatedButton(
child: Text('Complete Registration'),
onPressed: () => _register(context),
),
),
);
}
}pushAndRemoveUntil - Clearing the Entire Stack
Sometimes you want to clear not just one page, but the entire stack.
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomePage()),
(route) => false, // Remove all pages
);This is used to redirect the user to a completely new flow. For example, when logging out, all pages are cleared, only the login screen remains.
Logout Example
void logout(BuildContext context) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => LoginPage()),
(route) => false,
);
}Named Routes
For larger projects, writing MaterialPageRoute every time is tedious. You can use named routes.
Defining Routes in main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/profile': (context) => ProfilePage(),
'/settings': (context) => SettingsPage(),
},
);
}
}Navigation with Named Routes
// Normal navigation
Navigator.pushNamed(context, '/detail');
// Prevent back navigation
Navigator.pushReplacementNamed(context, '/home');
// Clear stack
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false,
);Sending Data with Named Routes
// Sending data
Navigator.pushNamed(
context,
'/detail',
arguments: {
'id': 123,
'name': 'John',
},
);
// Receiving data
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map;
return Scaffold(
appBar: AppBar(title: Text(args['name'])),
body: Center(
child: Text('ID: ${args['id']}'),
),
);
}
}Navigation with Animation
You can add different transition animations:
Slide from Right (Default)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage()),
);Slide from Bottom
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(),
fullscreenDialog: true,
),
);Custom Animation
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => DetailPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 1.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
var tween = Tween(begin: begin, end: end).chain(
CurveTween(curve: curve),
);
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);Intercepting Back Button
Performing custom action when back button is pressed on Android:
class MyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// Ask user
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Are you sure you want to exit?'),
actions: [
TextButton(
child: Text('No'),
onPressed: () => Navigator.pop(context, false),
),
TextButton(
child: Text('Yes'),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
return shouldPop ?? false;
},
child: Scaffold(
appBar: AppBar(title: Text('Page')),
body: Center(child: Text('Content')),
),
);
}
}Practical Tips
1. Use Context Correctly
// ❌ Wrong - build context usage
Navigator.push(context, ...);
// ✅ Correct - proper context
Builder(
builder: (BuildContext context) {
return ElevatedButton(
onPressed: () => Navigator.push(context, ...),
child: Text('Go'),
);
},
)2. Using Async/Await
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectPage()),
);
if (result != null) {
print('Result: $result');
}3. Avoid Unnecessary Navigator Usage
// ❌ Unnecessary - within the same page
Navigator.push(context, MaterialPageRoute(...));
// ✅ Use state update
setState(() {
selectedIndex = 1;
});Summary
Navigation in Flutter:
- push(): Open new page
- pop(): Go back
- pushReplacement(): Prevent back navigation
- pushAndRemoveUntil(): Clear stack
- pushNamed(): Use named routes
Data Transfer:
- Send data forward using constructor
- Return data using
pop(context, data)
Back Stack Management:
- Use
pushReplacementafter Login/Register - Clear stack with
pushAndRemoveUntilwhen logging out
Navigator is one of Flutter's most fundamental features. Once you get used to it, it's very easy to use!
Stuck with navigation?
See you in the next article! 🚀