tappeddev / injector

Simple dependency injection for Dart. 💉
Apache License 2.0
77 stars 7 forks source link

Allow an async dependency builder? #35

Closed saibotma closed 1 year ago

saibotma commented 2 years ago

Maybe this is an antipattern, however i once had the use case where i wanted to register a dependency by using an async dependency builder:

injector.registerSingleton<Calculation>(() async {
  return await createCalculation();
});

Unfortunately, this does not work at the moment. I am open for discussing this topic.

JulianBissekkou commented 2 years ago

I would not say that this is an antipattern, it's just something that is harder to do for service locators/dependency injections. I thought about this a lot and I also had this issue a couple of times. Here are some thoughts about this:

Dependency takes care of the async initializations

In most cases, my dependency took care of handling the async stuff by themselves to avoid dealing with this complexity in the dependency injection.

For example, we have a Repository that works with Hive which is initialized asynchronously. In my case, the Repository will initialize Hive maybe by using a callback.

Just some pseudo code:

injector.registerSingleton(() {
  // openbox is async.
  return Respository(initHive: () => Hive.openbox());
});

This is not an elegant solution if you have the use case a lot because it requires you to handle that init stuff in every class again.

Wrappers for initializing async dependencies

Let's say you register a class that comes from another framework where you have no control if the creation is async or not. You can create a simple wrapper around that dependency. Something like an "AsyncContainer".

// implementation is skipped.
class AsyncContainer<T> {
  Future<T> get value;
  AsyncContainer(Future<T> Function() factory);
}

injector.registerDependency<AsyncContainer<MyClass>>(() {
  return AsyncContainer(() => MyClass.createInstanceAsync(userService: injector.get()));
});

final container = await injector.get<AsyncContainer<MyClass>>().value;

This solution is also not that elegant because you now have to make sure you get all your async dependencies using AsyncContainer.

Adding support for Async factories in injector

We probably need something like injector.registerSingletonAsync<T>(/* factory method */), injector.registerDependencyAsync<T>(/* factory method */) or injector.registerAsyncFactory<T>(/* factory method */)

When getting the dependency you have to handle the case that the dependency is not yet initialized. So we need something like Future<T> getAsync<T>() and if the dependency that you try to get here is not async we have to throw. (or the other way around if you use get<T>() and it's not async).

Problems with async dependencies

GetIt has other functions like GetIt.allReady() which waits until all async factories are resolved. I think this is really dangerous because if you sometimes register an async dependency that takes longer to initialize and you call GetIt.ready() before hiding a splash screen or even before starting your flutter app, you will probably block the user. A better solution for this would be to handle this explicitly because the interface of that dependency force you to do this which brings me back to the first point. " >Dependency takes care of the async initializations Example:

injector.registerSingletonAsync(() async {
    final myService = MyService();
    await myService.init();
    return myService;
});

await injector.waitUntilAllDependenciesAreReady();
runApp(MyApp());

Maybe it makes sense to show a loading indicator to your users and call MyService.init at some point in the future. Maybe only then when you need it which might be when the user opens a specific screen. This can be controlled by whatever class is using MyService. Maybe a redux action, maybe a Bloc or a Provider.

To summarize:

Architecture wise there is a lot that can be done wrong with async dependencies, but it's up to the consumer of the package to decide this. I am down for implementing this into this package because many users asking for this.

JamesMcIntosh commented 2 years ago

To inject async dependencies I used this wrapper which acts as a factory to build the dependency. The extra static sync method I used in my tests.

typedef AsyncBuilder<T> = Future<T> Function();

typedef NonAsyncBuilder<T> = T Function();

class AsyncProvider<T> {

  AsyncBuilder<T> builder;

  AsyncProvider(this.builder);

  Future<T> get get => builder();

  static AsyncProvider<T> sync<T>(NonAsyncBuilder<T> builder) {
    return AsyncProvider<T>(() async => builder());
  }

}

i.e.

injector.registerSingleton<AsyncProvider<UserService>>(() => AsyncProvider<UserService>(() => UserService.get()));

class MyClass {

  final AsyncProvider<UserService> userServiceProvider;

  MyClass() : this.userServiceProvider = Injector.appInstance.get<AsyncProvider<UserService>>();

  Future<void> doSomething() async {
    final UserService userService = await userServiceProvider.get;
    ...
  }

}