Question #327EasyNavigation & Routing

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
RouteSettings
to pass data with named routes.

dart
// 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

dart
import '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

dart
import '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

dart
import '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

MethodUse CaseComplexityType Safety
Constructor ParametersSimple data passingLowHigh
Named Routes ArgumentsModerate complexityMediumMedium
ModalRoute.of()Dynamic route handlingMediumLow
Return Data (pop)Getting results backLowHigh
go_routerWeb apps, deep linkingMediumHigh
Provider/RiverpodShared app stateHighHigh
Bloc/GetXComplex state managementHighHigh

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

Official Documentation