Passing Data Between Screens in Flutter
In Flutter, applications commonly contain multiple screens such as login, home, product list, product details, profile, cart, checkout, and settings. When moving from one screen to another, you often need to send information from the current screen to the next screen. Flutter supports several ways to pass data between screens.
Flutter treats screens as routes, and the Navigator manages these routes as a stack. Data can be passed directly through widget constructors, through RouteSettings, through named-route arguments, or returned to the previous screen using Navigator.pop().
1. What Does Passing Data Between Screens Mean?
Passing data between screens means sending information from one route/widget to another route/widget during navigation.
For example, suppose a product list contains several products. When the user taps a product, the product's ID, name, price, image, and description can be sent to the product-detail screen.
Product List Screen
|
| Product data
v
Product Detail Screen
Flutter's official navigation cookbook demonstrates passing an object such as a Todo from one screen to another using Navigator.push(). Flutter: Send data to a new screen
2. Common Ways to Pass Data Between Screens
| Method | Use Case | Example |
|---|
| Constructor parameters | Simple and strongly typed data transfer | Pass a User or Product object |
| RouteSettings | Pass data while creating a route | RouteSettings(arguments: data) |
| Named route arguments | Passing data through named routes | pushNamed() |
| Return data with pop | Sending data back to the previous screen | Navigator.pop(context, result) |
| State management | Sharing data across many widgets/screens | Provider, Riverpod, Bloc, etc. |
3. Passing Data Using a Constructor
The simplest and most commonly useful approach is to pass data directly through the destination screen's constructor.
Example: Passing a String
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
const String userName = 'Manish';
return Scaffold(
appBar: AppBar(
title: const Text('Home Screen'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(
name: userName,
),
),
);
},
child: const Text('Open Profile'),
),
),
);
}
}
class ProfileScreen extends StatelessWidget {
final String name;
const ProfileScreen({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Profile'),
),
body: Center(
child: Text(
'Welcome, $name',
style: const TextStyle(fontSize: 24),
),
),
);
}
}
How It Works
- The first screen stores the data.
Navigator.push() opens the second screen.
- The data is passed to the second screen's constructor.
- The second screen stores the value in a field.
- The value is displayed in the UI.
4. Passing Multiple Values
You can pass multiple values through a constructor.
class ProfileScreen extends StatelessWidget {
final String name;
final int age;
final String email;
const ProfileScreen({
super.key,
required this.name,
required this.age,
required this.email,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(name),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: $name'),
Text('Age: $age'),
Text('Email: $email'),
],
),
),
);
}
}
Navigation code:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileScreen(
name: 'Manish',
age: 25,
email: '[email protected]',
),
),
);
5. Passing a Custom Object Between Screens
For real-world applications, passing a model object is usually cleaner than passing many individual values.
Create a Model Class
class Product {
final int id;
final String name;
final double price;
final String description;
const Product({
required this.id,
required this.name,
required this.price,
required this.description,
});
}
Create Product Data
final product = Product(
id: 101,
name: 'Flutter Course',
price: 4999,
description: 'Complete Flutter development course.',
);
Pass Product to Another Screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailsScreen(
product: product,
),
),
);
Receive Product
class ProductDetailsScreen extends StatelessWidget {
final Product product;
const ProductDetailsScreen({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Text('Product ID: ${product.id}'),
Text('Price: ₹${product.price}'),
const SizedBox(height: 10),
Text(product.description),
],
),
),
);
}
}
This pattern is useful for product details, user profiles, blog posts, orders, courses, messages, and other object-based data.
6. Practical Example: Todo List to Todo Details
Suppose an application displays a list of tasks. When the user taps a task, the selected task should be displayed on a details screen.
class Todo {
final String title;
final String description;
const Todo(
this.title,
this.description,
);
}
Todo List Screen
class TodoListScreen extends StatelessWidget {
TodoListScreen({super.key});
final List<Todo> todos = const [
Todo(
'Learn Flutter',
'Study Flutter widgets and navigation.',
),
Todo(
'Practice Dart',
'Practice classes, inheritance and collections.',
),
Todo(
'Build Project',
'Create a complete Flutter application.',
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo List'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
subtitle: Text(todo.description),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TodoDetailsScreen(
todo: todo,
),
),
);
},
);
},
),
);
}
}
Todo Details Screen
class TodoDetailsScreen extends StatelessWidget {
final Todo todo;
const TodoDetailsScreen({
super.key,
required this.todo,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Text(
todo.description,
style: const TextStyle(fontSize: 18),
),
),
);
}
}
The official Flutter cookbook uses this constructor-based pattern for passing a Todo object to a detail screen. View the official Flutter example
7. Passing Data Using RouteSettings
Another option is to pass an object through RouteSettings.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProductDetailsScreen(),
settings: RouteSettings(
arguments: product,
),
),
);
The destination screen can read the object using ModalRoute.of(context).
class ProductDetailsScreen extends StatelessWidget {
const ProductDetailsScreen({super.key});
@override
Widget build(BuildContext context) {
final product =
ModalRoute.of(context)!.settings.arguments as Product;
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Center(
child: Text(
'Price: ₹${product.price}',
),
),
);
}
}
The official Flutter cookbook documents this approach using RouteSettings(arguments: ...) and retrieving the value through ModalRoute.of(context). Flutter RouteSettings data-passing example
8. Passing Data Using Named Routes
Flutter also supports passing arguments through Navigator.pushNamed().
For example:
Navigator.pushNamed(
context,
'/product',
arguments: product,
);
The destination can retrieve the arguments:
class ProductScreen extends StatelessWidget {
const ProductScreen({super.key});
@override
Widget build(BuildContext context) {
final product =
ModalRoute.of(context)!.settings.arguments as Product;
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Center(
child: Text(
'₹${product.price}',
),
),
);
}
}
Flutter's official documentation supports arguments with Navigator.pushNamed(). However, Flutter currently notes that named routes are not recommended for most new applications; for many applications, Flutter recommends using Navigator with MaterialPageRoute or a routing package such as go_router. Flutter: Pass arguments to a named route
9. Using onGenerateRoute with Arguments
For applications that still use named routes, onGenerateRoute can centralize route creation and argument handling.
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/product') {
final product = settings.arguments as Product;
return MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
);
}
return MaterialPageRoute(
builder: (context) => const HomeScreen(),
);
},
);
Navigate to the route:
Navigator.pushNamed(
context,
'/product',
arguments: product,
);
10. Returning Data From the Second Screen
Data does not only move from the first screen to the second screen. A second screen can also return a result to the previous screen.
This is useful for:
- Selection screens
- Forms
- Editing screens
- Choosing a category
- Selecting a date
- Selecting a location
- Confirming an action
The second screen can return data using Navigator.pop(context, result). Flutter's official documentation demonstrates this pattern for returning a user's selection to the previous screen. Flutter: Return data from a screen
Second Screen
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Select Option'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(
context,
'Flutter Selected',
);
},
child: const Text('Select Flutter'),
),
),
);
}
}
Receive the Result
Future<void> openSelectionScreen(BuildContext context) async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => const SelectionScreen(),
),
);
if (result != null) {
print('Selected: $result');
}
}
11. Passing Data and Receiving Data Together
A common real-world pattern is to send an object to another screen, allow the user to modify it, and then return the updated value.
Model
class User {
final String name;
final String email;
const User({
required this.name,
required this.email,
});
}
Open Edit Screen
final updatedUser = await Navigator.push<User>(
context,
MaterialPageRoute(
builder: (context) => EditUserScreen(
user: user,
),
),
);
if (updatedUser != null) {
print(updatedUser.name);
print(updatedUser.email);
}
Edit Screen
class EditUserScreen extends StatelessWidget {
final User user;
const EditUserScreen({
super.key,
required this.user,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit User'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
final updatedUser = User(
name: 'Updated User',
email: user.email,
);
Navigator.pop(
context,
updatedUser,
);
},
child: const Text('Save'),
),
),
);
}
}
12. Passing Data From a Form to Another Screen
Forms frequently collect information that must be displayed or processed on another screen.
class FormScreen extends StatefulWidget {
const FormScreen({super.key});
@override
State<FormScreen> createState() => _FormScreenState();
}
class _FormScreenState extends State<FormScreen> {
final nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
void submitForm() {
final name = nameController.text;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WelcomeScreen(
name: name,
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('User Form'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: submitForm,
child: const Text('Submit'),
),
],
),
),
);
}
}
class WelcomeScreen extends StatelessWidget {
final String name;
const WelcomeScreen({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Welcome'),
),
body: Center(
child: Text(
'Hello, $name!',
style: const TextStyle(fontSize: 24),
),
),
);
}
}
13. Passing Data From a List to a Detail Page
This is one of the most common patterns in mobile applications.
ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('₹${product.price}'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
},
);
},
)
The flow is:
Product List
|
| selected Product
v
Product Details
|
| user action
v
Return result (optional)
14. Passing Data With Type Safety
Using typed constructor parameters is useful because Dart can detect many incorrect data types during development.
class StudentScreen extends StatelessWidget {
final String name;
final int age;
const StudentScreen({
super.key,
required this.name,
required this.age,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('$name is $age years old'),
),
);
}
}
Navigation:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const StudentScreen(
name: 'Rahul',
age: 22,
),
),
);
Constructor-based data passing makes the required inputs visible directly in the destination widget's API.
15. Passing Lists Between Screens
You can pass collections such as List objects just like other Dart objects.
class ItemsScreen extends StatelessWidget {
final List<String> items;
const ItemsScreen({
super.key,
required this.items,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Items'),
),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(items[index]),
);
},
),
);
}
}
Pass the list:
final languages = [
'Dart',
'Flutter',
'Java',
'Kotlin',
];
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ItemsScreen(
items: languages,
),
),
);
16. Passing Maps Between Screens
A Map can also be passed, although a model class is often preferable when the data has a defined structure.
final userData = {
'name': 'Manish',
'email': '[email protected]',
'role': 'Student',
};
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserScreen(
userData: userData,
),
),
);
Receive the map:
class UserScreen extends StatelessWidget {
final Map<String, String> userData;
const UserScreen({
super.key,
required this.userData,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Text(userData['name'] ?? ''),
Text(userData['email'] ?? ''),
Text(userData['role'] ?? ''),
],
),
);
}
}
17. Passing Data Using go_router
For applications with more advanced navigation requirements, Flutter documentation recommends considering a routing package such as go_router. This is especially useful when an application needs structured routing and deep-link handling.
A simplified example of passing an extra object with go_router is:
context.push(
'/details',
extra: product,
);
The destination route can read the extra data:
GoRoute(
path: '/details',
builder: (context, state) {
final product = state.extra as Product;
return ProductDetailsScreen(
product: product,
);
},
)
For complex applications, choose a routing approach that matches the application's navigation and deep-link requirements. Flutter Navigation and Routing Documentation
18. Passing Data vs Shared State Management
Directly passing data is suitable when one screen needs to send information to another screen. When many unrelated widgets or screens need access to the same changing data, a state-management solution may be more appropriate.
| Requirement | Possible Approach |
|---|
| Send one value to the next screen | Constructor parameter |
| Send a model object to a details screen | Constructor parameter |
| Return a selection | Navigator.pop() |
| Use named routes with arguments | pushNamed() + arguments |
| Share changing state across many widgets | State management |
| Complex navigation and deep links | Router or routing package |
19. Complete Practical Example
The following example demonstrates a product list, passing a product object to the details screen, and returning a result from the details screen.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Passing Data Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
),
useMaterial3: true,
),
home: const ProductListScreen(),
);
}
}
class ProductListScreen extends StatelessWidget {
const ProductListScreen({super.key});
static const products = [
Product(
id: 1,
name: 'Flutter Course',
price: 4999,
),
Product(
id: 2,
name: 'Dart Course',
price: 3999,
),
Product(
id: 3,
name: 'UI/UX Course',
price: 2999,
),
];
Future<void> openProduct(
BuildContext context,
Product product,
) async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) {
return ProductDetailsScreen(
product: product,
);
},
),
);
if (result != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
leading: CircleAvatar(
child: Text('${product.id}'),
),
title: Text(product.name),
subtitle: Text(
'₹${product.price.toStringAsFixed(0)}',
),
trailing: const Icon(
Icons.arrow_forward_ios,
),
onTap: () {
openProduct(
context,
product,
);
},
);
},
),
);
}
}
class ProductDetailsScreen extends StatelessWidget {
final Product product;
const ProductDetailsScreen({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: Theme.of(context)
.textTheme
.headlineSmall,
),
const SizedBox(height: 12),
Text(
'Product ID: ${product.id}',
),
const SizedBox(height: 8),
Text(
'Price: ₹${product.price.toStringAsFixed(0)}',
),
const Spacer(),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pop(
context,
'${product.name} added to cart',
);
},
child: const Text('Add to Cart'),
),
),
],
),
),
);
}
}
Complete Flow
- The product list screen contains product objects.
- The user taps a product.
Navigator.push() opens the details screen.
- The selected
Product object is passed through the constructor.
- The details screen displays the product information.
- The user taps Add to Cart.
- The details screen calls
Navigator.pop() with a result.
- The list screen receives the result using
await Navigator.push().
- A
SnackBar displays the returned result.
20. Common Mistakes
Mistake 1: Forgetting Required Constructor Data
class DetailsScreen extends StatelessWidget {
final String title;
const DetailsScreen({
super.key,
required this.title,
});
}
When navigating, make sure the required value is provided.
Mistake 2: Unsafe Type Casting
final product =
ModalRoute.of(context)!.settings.arguments as Product;
If the route receives a different object or no argument, this can cause a runtime error. Validate route arguments when using dynamic argument mechanisms.
Mistake 3: Passing Too Much Data
If a screen requires many unrelated values, consider creating a model class rather than passing a large number of individual parameters.
Mistake 4: Using Global Variables for Simple Screen-to-Screen Data
Global variables can make data flow difficult to understand. For simple navigation, direct constructor parameters are usually easier to follow.
Mistake 5: Confusing Navigation With State Management
Passing a value to the next screen is not the same as managing application-wide state. Use the appropriate architecture for data that needs to be shared and updated across many parts of an application.
21. Best Practices
- Prefer constructor parameters for straightforward screen-to-screen data.
- Use model classes when passing structured data.
- Use
required parameters for values the destination screen cannot work without.
- Use typed
Navigator.push<T>() when expecting a result.
- Use
Navigator.pop(context, result) to return data.
- Keep data models separate from UI widgets in larger applications.
- Avoid unnecessarily passing large amounts of unrelated data.
- For complex navigation and deep linking, consider Router-based navigation or a routing package.
- Validate dynamically passed route arguments when using
RouteSettings or named-route arguments.
22. Constructor vs RouteSettings vs Named Route Arguments
| Feature | Constructor | RouteSettings | Named Route Arguments |
|---|
| Easy to understand | Yes | Moderate | Moderate |
| Strongly typed | Yes | Requires casting | Requires casting |
| Good for model objects | Yes | Yes | Yes |
| Works with direct Navigator | Yes | Yes | No |
| Works with named routes | Not directly | Yes | Yes |
| Recommended for simple screen data | Yes | Useful alternative | Available, but named routes have limitations |
23. Interview Questions
Q1. How do you pass data from one screen to another in Flutter?
You can pass data through the destination widget's constructor and then navigate using Navigator.push().
Q2. How do you pass an object between screens?
Create a Dart model class and pass an instance of that class through the destination screen's constructor.
Q3. How do you return data from a second screen?
Use Navigator.pop(context, result) on the second screen and await the result of Navigator.push() on the first screen.
Q4. What is RouteSettings used for?
RouteSettings can carry route metadata, including an arguments object that can be retrieved by the destination route.
Q5. What is the difference between Navigator.push() and Navigator.pop()?
Navigator.push() adds a route to the navigation stack, while Navigator.pop() removes the current route and can optionally return a result.
Q6. Can a List be passed between screens?
Yes. Lists are Dart objects and can be passed through constructors or other supported argument mechanisms.
Q7. When should state management be used?
State management becomes useful when data needs to be shared, observed, and updated across multiple unrelated widgets or application sections.
24. Practice Exercises
- Create a login screen and pass the username to a home screen.
- Create a product list and pass a complete product object to a details screen.
- Create a student list and display the selected student's information.
- Create a settings screen that returns a selected theme name to the previous screen.
- Create an edit-profile screen that receives a user object and returns an updated user object.
- Create a shopping-cart screen that receives a list of products.
- Create a category-selection screen and return the selected category.
- Create a form screen and pass submitted form data to a confirmation screen.
25. Quick Revision
| Concept | Syntax |
|---|
| Open screen | Navigator.push() |
| Pass constructor data | Screen(data: value) |
| Pass route arguments | RouteSettings(arguments: value) |
| Named route arguments | Navigator.pushNamed(..., arguments: value) |
| Read route arguments | ModalRoute.of(context)!.settings.arguments |
| Return data | Navigator.pop(context, result) |
| Receive returned data | final result = await Navigator.push<T>(...) |
26. Key Takeaways
- Flutter screens are represented by routes.
- The
Navigator manages the route stack.
- Constructor parameters are a simple and strongly typed way to pass data.
- Custom Dart objects can be passed directly between screens.
RouteSettings can be used to attach arguments to a route.
- Named routes support arguments, although Flutter does not recommend named routes for most new applications.
Navigator.pop() can return a result to the previous screen.
- For complex navigation and deep linking, Router-based solutions or routing packages can be used.
- Use state-management solutions when data needs to be shared beyond a simple navigation flow.
27. Official Flutter Resources
28. Flutter Training Resources