MultiProvider & ProxyProvider
Managing Multiple Providers
Real-world Flutter applications rarely depend on a single provider. You will typically have authentication state, user profile data, API services, theme preferences, and many other pieces of state that need to be available throughout your widget tree. Provider offers two powerful tools for managing this complexity: MultiProvider for organizing multiple providers, and ProxyProvider for creating providers that depend on other providers.
MultiProvider: Clean Organization
Without MultiProvider, nesting multiple providers leads to deeply indented, hard-to-read code. MultiProvider flattens this nesting into a clean, readable list.
Without MultiProvider (Nested)
// Deeply nested — hard to read and maintain
Widget build(BuildContext context) {
return ChangeNotifierProvider<AuthModel>(
create: (_) => AuthModel(),
child: ChangeNotifierProvider<ThemeModel>(
create: (_) => ThemeModel(),
child: ChangeNotifierProvider<CartModel>(
create: (_) => CartModel(),
child: Provider<ApiService>(
create: (_) => ApiService(),
child: const MyApp(),
),
),
),
);
}
With MultiProvider (Flat)
// Clean and readable — same result, better structure
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
ChangeNotifierProvider<CartModel>(create: (_) => CartModel()),
Provider<ApiService>(create: (_) => ApiService()),
],
child: const MyApp(),
);
}
MultiProvider list matters. Providers are created top-to-bottom, and they can only access providers declared above them in the list. If CartModel depends on AuthModel, AuthModel must come first.
ProxyProvider: Dependent Providers
A ProxyProvider creates a provider whose value depends on another provider. When the dependency changes, the proxy provider automatically updates its value. This is essential for building service layers where one service needs data from another.
Basic ProxyProvider
class AuthModel extends ChangeNotifier {
String? _token;
String? get token => _token;
bool get isLoggedIn => _token != null;
void login(String token) {
_token = token;
notifyListeners();
}
void logout() {
_token = null;
notifyListeners();
}
}
class ApiService {
final String? authToken;
ApiService({this.authToken});
Map<String, String> get headers => {
'Content-Type': 'application/json',
if (authToken != null) 'Authorization': 'Bearer \$authToken',
};
Future<Map<String, dynamic>> fetchData(String endpoint) async {
// Uses authToken in headers automatically
final response = await http.get(
Uri.parse('https://api.example.com/\$endpoint'),
headers: headers,
);
return jsonDecode(response.body);
}
}
// Setup: ApiService depends on AuthModel
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<AuthModel>(
create: (_) => AuthModel(),
),
// ProxyProvider rebuilds ApiService when AuthModel changes
ProxyProvider<AuthModel, ApiService>(
update: (context, auth, previousApi) => ApiService(
authToken: auth.token,
),
),
],
child: const MyApp(),
);
}
In this example, every time AuthModel notifies its listeners (e.g., after login or logout), the ProxyProvider creates a new ApiService instance with the updated auth token. Any widget watching ApiService will automatically receive the new instance.
ProxyProvider2 and ProxyProvider3
When a provider depends on two or three other providers, use ProxyProvider2 or ProxyProvider3. These variants accept multiple type parameters and provide all dependencies in the update callback.
ProxyProvider2 — Two Dependencies
class UserRepository {
final ApiService api;
final CacheService cache;
UserRepository({required this.api, required this.cache});
Future<User> getUser(String id) async {
// Check cache first
final cached = cache.get('user_\$id');
if (cached != null) return User.fromJson(cached);
// Fetch from API if not cached
final data = await api.fetchData('users/\$id');
cache.set('user_\$id', data);
return User.fromJson(data);
}
}
// Setup with ProxyProvider2
MultiProvider(
providers: [
ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
Provider<CacheService>(create: (_) => CacheService()),
ProxyProvider<AuthModel, ApiService>(
update: (_, auth, __) => ApiService(authToken: auth.token),
),
// UserRepository depends on BOTH ApiService and CacheService
ProxyProvider2<ApiService, CacheService, UserRepository>(
update: (_, api, cache, __) => UserRepository(
api: api,
cache: cache,
),
),
],
child: const MyApp(),
)
ProxyProvider3 — Three Dependencies
class AnalyticsService {
final AuthModel auth;
final ApiService api;
final AppConfig config;
AnalyticsService({
required this.auth,
required this.api,
required this.config,
});
void trackEvent(String event) {
if (!config.analyticsEnabled) return;
api.fetchData('analytics/track?event=\$event&user=\${auth.token}');
}
}
// ProxyProvider3 with three dependencies
ProxyProvider3<AuthModel, ApiService, AppConfig, AnalyticsService>(
update: (_, auth, api, config, __) => AnalyticsService(
auth: auth,
api: api,
config: config,
),
)
ChangeNotifierProxyProvider
When your dependent provider is a ChangeNotifier (not just a plain object), use ChangeNotifierProxyProvider. This variant properly disposes the notifier when it is no longer needed and provides the previous instance for state preservation.
ChangeNotifierProxyProvider
class UserProfileModel extends ChangeNotifier {
final ApiService _api;
User? _user;
bool _isLoading = false;
UserProfileModel({required ApiService api}) : _api = api;
User? get user => _user;
bool get isLoading => _isLoading;
// Update the API service when auth changes
void updateApi(ApiService newApi) {
// Re-fetch user data with new credentials
if (_user != null) {
loadUser(_user!.id);
}
}
Future<void> loadUser(String id) async {
_isLoading = true;
notifyListeners();
try {
final data = await _api.fetchData('users/\$id');
_user = User.fromJson(data);
} catch (e) {
_user = null;
}
_isLoading = false;
notifyListeners();
}
}
// Setup: UserProfileModel depends on ApiService AND is a ChangeNotifier
MultiProvider(
providers: [
ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
ProxyProvider<AuthModel, ApiService>(
update: (_, auth, __) => ApiService(authToken: auth.token),
),
ChangeNotifierProxyProvider<ApiService, UserProfileModel>(
create: (context) => UserProfileModel(
api: context.read<ApiService>(),
),
update: (context, api, previous) {
// Preserve existing state, update dependency
previous!.updateApi(api);
return previous;
},
),
],
child: const MyApp(),
)
update callback of ChangeNotifierProxyProvider, always return the previous instance when possible. Creating a new instance every time will lose accumulated state (loaded data, flags, etc.) and can cause infinite rebuild loops.
Lazy Loading Providers
By default, providers in Flutter are lazy — they are not created until a widget first accesses them. This is beneficial for performance because providers for features the user has not visited yet will not consume resources.
Controlling Lazy Loading
MultiProvider(
providers: [
// Lazy (default) — created when first accessed
ChangeNotifierProvider<CartModel>(
create: (_) => CartModel(),
// lazy: true is the default
),
// Eager — created immediately when MultiProvider builds
ChangeNotifierProvider<AuthModel>(
create: (_) => AuthModel()..checkStoredSession(),
lazy: false, // Created immediately
),
// Eager — useful for providers that need initialization
Provider<DatabaseService>(
create: (_) => DatabaseService()..initialize(),
lazy: false, // Start DB connection right away
dispose: (_, db) => db.close(), // Clean up on dispose
),
],
child: const MyApp(),
)
lazy: false for providers that perform initialization work (like checking stored auth tokens or opening database connections). Keep all other providers lazy to avoid unnecessary resource usage at startup.
Provider Scope and Disposal
Providers are scoped to the widget tree where they are declared. When the widget that created a provider is removed from the tree, the provider is automatically disposed. This is particularly important for ChangeNotifier providers, which need cleanup to prevent memory leaks.
Scoped Providers
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
// App-level providers — live for the entire app lifetime
providers: [
ChangeNotifierProvider(create: (_) => AuthModel()),
ChangeNotifierProvider(create: (_) => ThemeModel()),
],
child: MaterialApp(
routes: {
'/': (_) => const HomePage(),
'/shop': (_) => ChangeNotifierProvider(
// Page-level provider — disposed when leaving /shop
create: (_) => CartModel(),
child: const ShopPage(),
),
'/chat': (_) => ChangeNotifierProvider(
// Page-level provider — disposed when leaving /chat
create: (_) => ChatModel(),
child: const ChatPage(),
),
},
),
);
}
}
Practical Example: Auth + User Profile Chain
Let’s build a complete, real-world example that chains authentication, API service, user repository, and user profile together:
Complete Provider Chain
// 1. Auth provides the token
class AuthModel extends ChangeNotifier {
String? _token;
User? _currentUser;
String? get token => _token;
User? get currentUser => _currentUser;
bool get isLoggedIn => _token != null;
Future<void> login(String email, String password) async {
final response = await http.post(
Uri.parse('https://api.example.com/login'),
body: jsonEncode({'email': email, 'password': password}),
);
final data = jsonDecode(response.body);
_token = data['token'];
_currentUser = User.fromJson(data['user']);
notifyListeners();
}
void logout() {
_token = null;
_currentUser = null;
notifyListeners();
}
}
// 2. ApiService depends on AuthModel for the token
class ApiService {
final String? authToken;
ApiService({this.authToken});
Future<dynamic> get(String path) async {
final response = await http.get(
Uri.parse('https://api.example.com/\$path'),
headers: {
'Content-Type': 'application/json',
if (authToken != null) 'Authorization': 'Bearer \$authToken',
},
);
return jsonDecode(response.body);
}
}
// 3. UserRepository depends on ApiService
class UserRepository {
final ApiService _api;
UserRepository({required ApiService api}) : _api = api;
Future<UserProfile> getProfile() async {
final data = await _api.get('profile');
return UserProfile.fromJson(data);
}
Future<List<Order>> getOrders() async {
final data = await _api.get('orders');
return (data as List).map((e) => Order.fromJson(e)).toList();
}
}
// 4. Wire everything together
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
// Layer 1: Auth (independent)
ChangeNotifierProvider<AuthModel>(
create: (_) => AuthModel(),
lazy: false, // Check stored session immediately
),
// Layer 2: ApiService (depends on Auth)
ProxyProvider<AuthModel, ApiService>(
update: (_, auth, __) => ApiService(
authToken: auth.token,
),
),
// Layer 3: UserRepository (depends on ApiService)
ProxyProvider<ApiService, UserRepository>(
update: (_, api, __) => UserRepository(api: api),
),
],
child: MaterialApp(
home: Consumer<AuthModel>(
builder: (context, auth, _) {
return auth.isLoggedIn
? const DashboardPage()
: const LoginPage();
},
),
),
);
}
}
// 5. Usage in widgets
class DashboardPage extends StatelessWidget {
const DashboardPage({super.key});
@override
Widget build(BuildContext context) {
final user = context.select<AuthModel, User?>(
(auth) => auth.currentUser,
);
return Scaffold(
appBar: AppBar(
title: Text('Welcome, \${user?.name ?? "User"}'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => context.read<AuthModel>().logout(),
),
],
),
body: FutureBuilder<UserProfile>(
future: context.read<UserRepository>().getProfile(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: \${snapshot.error}'));
}
final profile = snapshot.data!;
return ProfileCard(profile: profile);
},
),
);
}
}