How do you pass data between screens in Flutter?
#navigation#data-passing#routing#arguments
Answer
Overview
Flutter provides multiple methods to pass data between screens, ranging from simple constructor parameters to advanced state management solutions. The choice depends on your app's complexity and requirements.
Methods for Passing Data
1. Constructor Parameters (Recommended for Simple Cases)
The most straightforward method is passing data through widget constructors.
dart// Define the receiving screen with parameters class DetailScreen extends StatelessWidget { final String title; final int id; final Product product; const DetailScreen({ Key? key, required this.title, required this.id, required this.product, }) : super(key: key); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: Center( child: Column( children: [ Text('Product ID: $id'), Text('Name: ${product.name}'), Text('Price: \$${product.price}'), ], ), ), ); } } // Navigate and pass data class HomeScreen extends StatelessWidget { Widget build(BuildContext context) { final product = Product(id: 1, name: 'Flutter Book', price: 29.99); return Scaffold( body: ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailScreen( title: 'Product Details', id: product.id, product: product, ), ), ); }, child: Text('View Details'), ), ); } } class Product { final int id; final String name; final double price; Product({required this.id, required this.name, required this.price}); }
2. Named Routes with Arguments
Using
text
RouteSettingsdart// Define routes in MaterialApp class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( initialRoute: '/', onGenerateRoute: (settings) { if (settings.name == '/details') { final args = settings.arguments as Product; return MaterialPageRoute( builder: (context) => DetailScreen(product: args), ); } // Handle other routes return null; }, ); } } // Navigate with arguments Navigator.pushNamed( context, '/details', arguments: product, ); // Receive in DetailScreen class DetailScreen extends StatelessWidget { final Product product; const DetailScreen({required this.product}); Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(product.name)), body: Text('Price: \$${product.price}'), ); } }
3. ModalRoute.of() Method
Extract arguments from the current route.
dart// Navigate with arguments Navigator.pushNamed( context, '/details', arguments: { 'id': 123, 'title': 'Product', 'data': product, }, ); // Receive in destination screen class DetailScreen extends StatelessWidget { Widget build(BuildContext context) { final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>; final id = args['id'] as int; final title = args['title'] as String; final product = args['data'] as Product; return Scaffold( appBar: AppBar(title: Text(title)), body: Text('Product ID: $id, Name: ${product.name}'), ); } }
4. Returning Data from Screen
Navigate to a screen and receive data back when it's popped.
dart// Navigate and await result class HomeScreen extends StatefulWidget { State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { String? selectedOption; Future<void> _navigateAndGetResult() async { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => SelectionScreen(), ), ); if (result != null) { setState(() { selectedOption = result; }); } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Home')), body: Column( children: [ Text('Selected: ${selectedOption ?? "None"}'), ElevatedButton( onPressed: _navigateAndGetResult, child: Text('Select Option'), ), ], ), ); } } // Selection screen that returns data class SelectionScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Select')), body: ListView( children: [ ListTile( title: Text('Option A'), onTap: () { Navigator.pop(context, 'Option A'); }, ), ListTile( title: Text('Option B'), onTap: () { Navigator.pop(context, 'Option B'); }, ), ], ), ); } }
5. Using go_router with Path Parameters
dartimport 'package:go_router/go_router.dart'; final router = GoRouter( routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(), ), GoRoute( path: '/details/:id', builder: (context, state) { final id = state.pathParameters['id']!; final queryParam = state.uri.queryParameters['sort']; return DetailScreen(id: id, sort: queryParam); }, ), ], ); // Navigate with path parameters context.go('/details/123?sort=name'); // Navigate with extra data (complex objects) context.push('/details/123', extra: product); // Receive extra data GoRoute( path: '/details/:id', builder: (context, state) { final id = state.pathParameters['id']!; final product = state.extra as Product; return DetailScreen(id: id, product: product); }, ),
6. State Management Solutions
For complex apps, use state management to share data.
Using Provider
dartimport 'package:provider/provider.dart'; // Create a shared model class CartModel extends ChangeNotifier { final List<Product> _items = []; List<Product> get items => _items; void addItem(Product product) { _items.add(product); notifyListeners(); } } // Provide at app level class MyApp extends StatelessWidget { Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => CartModel(), child: MaterialApp( home: HomeScreen(), ), ); } } // Add item in one screen class HomeScreen extends StatelessWidget { Widget build(BuildContext context) { return ElevatedButton( onPressed: () { context.read<CartModel>().addItem(product); Navigator.push( context, MaterialPageRoute(builder: (context) => CartScreen()), ); }, child: Text('Add to Cart'), ); } } // Access in another screen class CartScreen extends StatelessWidget { Widget build(BuildContext context) { final cart = context.watch<CartModel>(); return ListView.builder( itemCount: cart.items.length, itemBuilder: (context, index) { return ListTile(title: Text(cart.items[index].name)); }, ); } }
Using Riverpod
dartimport 'package:flutter_riverpod/flutter_riverpod.dart'; // Define provider final selectedProductProvider = StateProvider<Product?>((ref) => null); // Set data in one screen class HomeScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return ElevatedButton( onPressed: () { ref.read(selectedProductProvider.notifier).state = product; Navigator.push( context, MaterialPageRoute(builder: (context) => DetailScreen()), ); }, child: Text('View Details'), ); } } // Read data in another screen class DetailScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final product = ref.watch(selectedProductProvider); return Scaffold( appBar: AppBar(title: Text(product?.name ?? 'Details')), body: Text('Price: \$${product?.price}'), ); } }
Comparison Table
| Method | Use Case | Complexity | Type Safety |
|---|---|---|---|
| Constructor Parameters | Simple data passing | Low | High |
| Named Routes Arguments | Moderate complexity | Medium | Medium |
| ModalRoute.of() | Dynamic route handling | Medium | Low |
| Return Data (pop) | Getting results back | Low | High |
| go_router | Web apps, deep linking | Medium | High |
| Provider/Riverpod | Shared app state | High | High |
| Bloc/GetX | Complex state management | High | High |
Best Practices
- Use constructor parameters for simple, direct data passing
- Use named routes when you need centralized route management
- Return data with pop() for form results and selections
- Use state management for data shared across multiple screens
- Avoid ModalRoute.of() for type-safe alternatives when possible
- Use go_router for web apps and complex navigation
- Keep data models immutable when passing between screens
- Validate data in receiving screen to handle null cases
Common Mistakes to Avoid
- Passing large objects that should be managed globally
- Not handling null or missing arguments
- Using arguments for data that changes frequently
- Mixing different data passing methods unnecessarily
- Forgetting to define required parameters in constructors