IoC-Container for Dart and Flutter
Navigation
- How is it different from Riverpod?
- How is it different from GetIt?
- Getting started
- Features
- Is it production ready?
- Credits
How is it different from Riverpod?
- It's not a state management solution
- Only 2 provider types
- No need to specify
dependencies
to achieve correct scoping
How is it different from GetIt?
- Type-safe factories (no more param1 and param2)
- Providers are registered at compile-time
- Ability to register the same type twice or more in type-safe manner.
Getting started
Define provider as global variable
class Counter {
final int count = 0;
}
final counterProvider = Provider((read) => Counter());
Create container - it holds all providables
final container = ProviderContainer();
Read provider
void main() {
/// Counter is created lazily and cached inside container
final counter = container.read(counterProvider);
final sameCounter = container.read(counterProvider);
print(counter == sameCounter); // true
print(counter.count); // 0
}
Features
Providers
There are 2 types of Providers in vessel
:
class Counter {}
class UserViewModel {
final int userId;
UserViewModel(this.userId);
}
// Provider
final counterProvider = Provider((_) => Counter());
// Factory provider
final userVmProvider = Provider.factory(
(_, int userId) => UserViewModel(userId),
);
The difference between these two is that Provider.factory
creates providers, while usual providers are self-contained.
Consider Provider
usage:
container.read(counterProvider); // Counter instance
And Provider.factory
:
final user100Provider = userVmProvider(100);
container.read(user100Provider);
or just:
container.read(userVmProvider(100)) // UserVM.userId === 100
Injecting providers
final cartRepositoryProvider = Provider(
(_) => CartRepository(),
);
final cartViewModelProvider = Provider.factory((read, int cartId) {
final repository = read(cartRepository);
return CartViewModel(
repository: repository,
cartId: cartId,
);
});
Disposing providers
final cartViewModelProvider = Provider(
(read) => CartViewModel(...),
dispose: (CartViewModel vm) => vm.dispose(),
);
Container has dispose
method, which disposes all providers within it.
container.dispose();
Overrides
You can override any provider with any other provider of compatible type.
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWith(mockUserRepositoryProvider)
]
);
container.read(userRepositoryProvider); // MockUserRepository
Example
Consider this:
class UserRepository {
User getById(int id) {
return User(id: id, isAdmin: false);
}
}
class UserProfileViewModel {
final UserRepository repository;
final int userId;
UserProfileViewModel({
required this.repository,
required this.userId,
});
String get isAdmin => repository.getById(userId).isAdmin;
}
final userRepositoryProvider = Provider(
(_) => UserRepository(),
);
final userProfileVmProvider = Provider.factory(
(read, int userId) => UserProfileViewModel(
userId: userId,
repository: read(userRepositoryProvider),
)
);
Here is the task: mock UserRepository, so getById always returns admin user. Easy:
class MockUserRepository implements UserRepository {
User getById(int id) {
return User(id: id, isAdmin: true);
}
}
final mockRepositoryProvider = Provider<UserRepository>(() => MockUserRepository());
final containerWithOverride = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWith(mockRepositoryProvider),
],
);
void main() {
final profileVm = containerWithOverride.read(userProfileVmProvider(1));
print(profileVm.isAdmin); // true
}
Scopes
Providers can be scoped:
final userProvider = Provider((_) => User(...));
final containerRoot = ProviderContainer();
final containerChild = ProviderContainer(
overrides: [userProvider.scope()],
parent: containerRoot,
);
void main() {
final rootUser = containerRoot.read(userProvider);
final childUser = containerChild.read(userProvider);
identical(rootUser, childUser); // false
}
provider.scope()
is essentially the same as provider.overrideWith(provider)
, so scoping and overriding are basically the same thing.
Provider becomes scoped, if any of its dependencies* gets scoped. Consider this example:
class Counter {
final int count;
Counter(this.count);
}
final provider1 = Provider((_) => Counter(1));
final provider2 = Provider((read) => Counter(read(provider1).count + 1));
final provider3 = Provider((read) => Counter(read(provider2).count + 3));
final container = ProviderContainer();
final containerChild = ProviderContainer.scoped(
[provider2],
parent: container,
);
void main() {
// now provider3 also scoped inside containerChild
final instance3 = containerChild.read(provider3);
final rootInstance3 = container.read(provider3);
identical(instance3, rootInstance3); // false
final instance1 = containerChild.read(provider1);
final rootInstance1 = container.read(provider1);
// provider1 doesn't have scoped dependencies, so it doesn't become scoped.
identical(instance1, rootInstance1); // true
}
Dependencies*
final provider1 = Provider((_) => Counter(1));
final provider2 = Provider((read) => Counter(read(provider1).count + 1));
final provider3 = Provider((read) => Counter(read(provider2).count + 3));
provider1
has no dependenciesprovider2
has single dependency - onprovider1
.provider3
has 2 dependencies:
- direct dependency on
provider2
- transitive dependency on
provider1
throughprovider2
Is it production ready?
Not enough testing have been done to consider this production ready. But I'm going to use it on production project.
Credits
The whole project inspired by riverpod, created by Remi Rousselet and community.