State management in Flutter often involves multiple providers working together. In many real-world applications, one provider may depend on another, or you may need to supply several providers to the widget tree at once.
This is where ProxyProvider and MultiProvider come in.
MultiProviderallows you to combine multiple providers in a clean and organized way.ProxyProviderallows you to create dependencies between providers, ensuring one provider can consume data from another.
Both are part of the provider package, which is one of the most widely used state management solutions in Flutter.
In this post, we’ll explore these concepts in depth with detailed explanations, examples, and best practices.
Why Do We Need ProxyProvider and MultiProvider?
Without MultiProvider
If you want to provide multiple values using the provider package, you might write something like this:
return Provider<A>(
create: (_) => A(),
child: Provider<B>(
create: (_) => B(),
child: Provider<C>(
create: (_) => C(),
child: MyApp(),
),
),
);
This works, but it becomes nested and hard to read as the number of providers grows.
With MultiProvider
Instead, you can use MultiProvider to flatten this structure:
return MultiProvider(
providers: [
Provider<A>(create: (_) => A()),
Provider<B>(create: (_) => B()),
Provider<C>(create: (_) => C()),
],
child: MyApp(),
);
Cleaner, right?
Why ProxyProvider?
Sometimes one provider depends on another. For example:
- You might have a
Userprovider and aUserRepositoryprovider. UserRepositorydepends on the current user.- If the
Userchanges,UserRepositoryshould also update.
In such cases, ProxyProvider helps by injecting one provider into another dynamically.
MultiProvider in Detail
What is MultiProvider?
MultiProvider is a widget that takes a list of providers and supplies them all at once to the widget tree. It’s essentially a convenience wrapper around nested providers.
Syntax
MultiProvider(
providers: [
Provider<A>(create: (_) => A()),
Provider<B>(create: (_) => B()),
],
child: MyApp(),
);
Example of MultiProvider
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
Provider(create: (_) => ApiService()),
],
child: MyApp(),
),
);
}
Here:
Counteris aChangeNotifierused for managing counter state.ApiServiceprovides backend API calls.- Both are available anywhere inside
MyApp.
When to Use MultiProvider?
- When your app uses more than one provider.
- When you want to avoid nesting providers manually.
- To improve readability and maintainability.
ProxyProvider in Detail
What is ProxyProvider?
ProxyProvider allows one provider to depend on another provider. Whenever the dependency provider changes, the proxy provider automatically rebuilds with the new value.
It is useful when the value of a provider depends on data from another provider.
Syntax
ProxyProvider<A, B>(
update: (context, a, b) => B(a),
)
Here:
Ais the dependency provider.Bis the proxy provider that depends onA.- The
updatemethod creates or updatesBwheneverAchanges.
Example: User and UserRepository
class User {
final String id;
User(this.id);
}
class UserRepository {
final User user;
UserRepository(this.user);
String fetchData() {
return "Data for user: ${user.id}";
}
}
void main() {
runApp(
MultiProvider(
providers: [
Provider<User>(create: (_) => User("12345")),
ProxyProvider<User, UserRepository>(
update: (_, user, __) => UserRepository(user),
),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final repository = Provider.of<UserRepository>(context);
return MaterialApp(
home: Scaffold(
body: Center(child: Text(repository.fetchData())),
),
);
}
}
Explanation:
Useris provided first.UserRepositoryis created usingProxyProvider, depending onUser.- If the
Userchanges,UserRepositoryupdates automatically.
Types of ProxyProvider
The provider package provides different variants of ProxyProvider depending on the number of dependencies.
1. ProxyProvider
Depends on one provider.
ProxyProvider<User, UserRepository>(
update: (_, user, __) => UserRepository(user),
)
2. ProxyProvider2
Depends on two providers.
ProxyProvider2<User, ApiService, UserRepository>(
update: (_, user, api, __) => UserRepository(user, api),
)
Here, UserRepository depends on both User and ApiService.
3. ProxyProvider3 and Beyond
Similarly, you can use ProxyProvider3, ProxyProvider4, and so on, up to ProxyProvider6.
Example with 3 dependencies:
ProxyProvider3<User, ApiService, Logger, UserRepository>(
update: (_, user, api, logger, __) => UserRepository(user, api, logger),
)
Combining ProxyProvider and MultiProvider
In real apps, you often use ProxyProvider inside MultiProvider.
Example: Auth Example
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthService()),
ProxyProvider<AuthService, ApiService>(
update: (_, auth, __) => ApiService(auth.token),
),
],
child: MyApp(),
),
);
}
Explanation:
AuthServiceprovides authentication details.ApiServicedepends onAuthServicefor a token.- If the token changes,
ApiServiceupdates automatically.
Best Practices
- Keep Providers Small and Focused
- Each provider should manage a single concern (e.g., authentication, API calls, cart state).
- Use MultiProvider for Clarity
- Always wrap multiple providers in
MultiProvider.
- Always wrap multiple providers in
- Use ProxyProvider for Dependencies
- Whenever one provider depends on another, use
ProxyProvider.
- Whenever one provider depends on another, use
- Avoid Unnecessary Dependencies
- Do not chain too many providers together unnecessarily. It complicates state flow.
- Test Your Providers
- Providers can be easily mocked and tested. Keep them independent of UI.
Common Mistakes
- Forgetting to Add ProxyProvider in MultiProvider
- If
ProxyProvideris not declared after its dependency provider, it will throw an error.
- If
- Using BuildContext in Providers
- Keep providers independent of UI context.
- Not Disposing Resources
- If your provider uses controllers or streams, dispose them properly in
dispose.
- If your provider uses controllers or streams, dispose them properly in
- Creating Large Provider Graphs
- Break them into smaller providers for readability.
Real-World Scenarios for ProxyProvider and MultiProvider
- Authentication + API Service
- Auth provider supplies tokens.
- API service provider uses the token.
- User Preferences + Theme Provider
- Preferences provider stores dark/light mode.
- Theme provider uses it to apply app theme.
- Cart + Product Service
- Cart provider stores selected items.
- Product service provider fetches product details.
- Localization
- Language provider supplies current language.
- Localization service uses it to provide translated strings.
Advanced Example: Chained Dependencies
void main() {
runApp(
MultiProvider(
providers: [
Provider(create: (_) => Config()),
ProxyProvider<Config, ApiService>(
update: (_, config, __) => ApiService(config.baseUrl),
),
ProxyProvider<ApiService, UserRepository>(
update: (_, api, __) => UserRepository(api),
),
ProxyProvider<UserRepository, UserManager>(
update: (_, repo, __) => UserManager(repo),
),
],
child: MyApp(),
),
);
}
Here, we chain multiple providers:
Config→ApiService→UserRepository→UserManager- Each depends on the previous one.
- Updates propagate automatically.
Performance Considerations
- ProxyProvider only rebuilds when dependencies change. This keeps performance optimized.
- Use
Consumerwidgets orSelectorto minimize widget rebuilds. - Avoid putting too many dependencies in a single provider.
Leave a Reply