Dependency injection in Flutter - repercussions of different approaches

2,074

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.

Share:
2,074
BeniaminoBaggins
Author by

BeniaminoBaggins

I dabble in developing web and mobile apps.

Updated on December 26, 2022

Comments

  • BeniaminoBaggins
    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 need ProviderContainer().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 like AuthenticationService(), or using Riverpod's ProviderContainer.read()?

  • Susan Thapa
    Susan Thapa over 2 years
    I 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
    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.