Dependency injection in Flutter - repercussions of different approaches
After looking around I can see that dependency injection in Flutter is mostly done by creating a new instance of the param to inject into the constructor like in my question:
final CreateUserViewModel createUserViewModel =
CreateUserViewModel(SavePasswordLocallyUseCase(), CreateUserUseCase());
And that this becomes very messy very fast if the injected classes also need their own params injected. Dependency injection packages mostly aim to solve this by setting up the classes with their required params once, and then you can just request the instance from the dependency injection package without ever needing to create its constructor params again. I believe the Riverpod version of this is indeed to use ProviderContainer.read()
if not in the UI layer. Lets say I want to instantiate a class which takes a repository in its constructor:
class SavePasswordLocallyUseCase implements ISavePasswordLocallyUseCase {
const SavePasswordLocallyUseCase(this._userRepository);
final IRegistrationRepository _userRepository;
String invoke(String password, String confirmPassword) {
return _userRepository.putPasswords(password, confirmPassword);
}
And the injected repository itself needs 2 constructor params:
class RegistrationRepository implements IRegistrationRepository {
const RegistrationRepository(
this._authenticationRemoteDataSource, this._registrationLocalDataSource);
}
final AuthenticationRemoteDataSource _authenticationRemoteDataSource;
final IRegistrationLocalDataSource _registrationLocalDataSource;
Instead of instantiating the class like this:
new RegistrationRepository(AuthenticationRemoteDataSource(X()), RegistrationLocalDataSource(Y()))
By creating a stock standard Provider
in Riverpod which instantiates the params once:
final registrationRepositoryProvider =
Provider.autoDispose<RegistrationRepository>((ref) {
ref.onDispose(() {
print('disposing');
});
return RegistrationRepository(
AuthenticationRemoteDataSource(X()), RegistrationLocalDataSource(Y()));
});
I can then allow classes to access the RegistrationRepository
like so:
ref.container.read(registrationRepositoryProvider);
Or without a ProviderReference:
ProviderContainer().read(registrationRepositoryProvider);
I'm still not sure about lazy loading and singletons in riverpod and if those options are possible. It might be more configurable to use Injectable for DI that is not in the View, which I am considering.
Comments
-
BeniaminoBaggins over 1 year
In Flutter, I feel a bit lost with how I create my params for classes, and knowing what is the best way to create those params. Usually, the params are just classes to inject into another class, to perform tasks. It doesn't really seem to matter how those params are created functionality-wise, since the code works with all manner of creation methods. I see online people talking about service locators, singletons, dependency injection. Riverpod states on the website "Providers are a complete replacement for patterns like Singletons, Service Locators, Dependency Injection or InheritedWidgets." So I guess I don't need a service locator, since I use Riverpod. However I can't find anything online on how I can inject a service with Riverpod providers. I can see you can read a provider with no context with
ProviderContainer().read
but is this for use as service injection? Is this a singleton so pretty much a service locator? In the Riverpod example, it states that you don't needProviderContainer().read
for Flutter, which kind of sounds like it isn't a replacement for anything like a service locator then:// Where the state of our providers will be stored. // Avoid making this a global variable, for testability purposes. // If you are using Flutter, you do not need this. final container = ProviderContainer();
Here is a code example, a field in a class which is a ViewModel which takes some use cases as params. Use cases here are domain layer actions classes which call repositories to do external stuff like API requests or local storage manipulations.
final CreateUserViewModel createUserViewModel = CreateUserViewModel(SavePasswordLocallyUseCase(), CreateUserUseCase()); ...
So I just literally created them in-line like
SavePasswordLocallyUseCase()
, in order to inject them. This is defnitely the easiest approach, with the least code. I guess it might be less efficient since it is creating a new one every time, though i don't see that usually making a visible difference. Will these params that are created in this manner be cleaned up by the garbage collector? What is the repercussion of doing this?If I had to inject a type
AuthenticationService
, should I be using a service locator or creating them inline likeAuthenticationService()
, or using Riverpod'sProviderContainer.read()
? -
Susan Thapa over 2 yearsI had the same question when I was implementing unidirectional architecture in flutter with riverpod. Like you I decided to go with injectable. Here is the link to the reddit post that has some discussion on this. Maybe it's helpful.
-
BeniaminoBaggins over 2 years@SusanThapa Great read. I went with Riverpod for DI. I agree the Riverpod documentation does not go into these questions we have. I see though that you went with Injectable in order to not manually create dependencies. Keen to see how that goes. I remain open to everything, and Injectable looks promising.