Skip to content

Extending Kotlin Inject Anvil for Assisted Factories

Image title

I recently started using Kotlin inject anvil in my kotlin multiplatform (KMP) projects for dependency injection. However when using assisted injection I noticed myself writing boilerplate code to bind assisted factory method interfaces. In the process of trying to not write all this boiler plate code, I ended up writing a library that extends kotlin inject anvil to generate the code necessary to bind the assisted factory interface. Learn how to use this extension to bind assisted factory method interfaces.

The Problem

I use a library called Decompose to manage navigation in my KMP projects which heavily relies on assisted injection to inject a ComponentContext into each business logic component (BloC). For this example, consider a simple interface for MoviesBloc and real implementation of one.

interface MoviesBloc {
  val state: StateFlow<State>
}

@Inject
@ContributesBinding(
  scope = AppScope::class,
  boundType = MoviesBloc::class
)
class RealMoviesBloc(
  @Assisted context: ComponentContext
  private val repository: MovieRepository,
): MoviesBloc, ComponentContext by context {
  override val state: StateFlow<State> = TODO()
}

Due to a recent contribution I made to kotlin-inject-anvil, to create a MoviesBloc elsewhere a lambda could be injected that takes the assisted parameter as an argument (ComponentContext) -> MoviesBloc. Coming from a background using dagger and anvil, I had become accustomed to a pattern of creating factory interfaces that would create assisted injected dependencies.

interface MoviesBloc {
  interface Factory {
    fun create(context: ComponentContext): MoviesBloc
  }
}

In order to bind this assisted factory in the dependency graph, a real implementation of this factory would need to be written injecting the lambda parameter.

@Inject
@ContributesBinding(
  scope = AppScope::class,
  boundType = MoviesBloc.Factory::class
)
class RealMoviesBlocFactory(
  private val realFactory: (ComponentContext) -> MoviesBloc
): MoviesBloc.Factory {

  override fun create(
    context: ComponentContext
  ): MoviesBloc = realFactory(context)
}

Finally, this factory interface can be injected into other classes to create instances of the MoviesBloc.

@Inject
@ContributesBinding(
  scope = AppScope::class,
  boundType = RootBloc::class
)
class RealRootBloc(
  @Assisted context: ComponentContext,
  private val moviesBlocFactory: MoviesBloc.Factory,
): RootBloc, ComponentContext by context {

  private fun createMoviesBloc(context: ComponentContext) = 
    moviesBlocFactory.create(context)
}

The RealMoviesBlocFactory seemed like unnecessary boiler plate code just to bind an assisted factory method, so I wrote my first extension of kotlin inject anvil to help remove the need to write all this glue code.

Assisted Factory Extension

Kotlin inject anvil was designed to be flexible by allowing one to extend the framework to generate components in situations just like this. Which is how the kotlin-inject-anvil-extensions library was created. On top of the usual setup of kotlin-inject-anvil, the extensions can be setup in a similar fashion with instructions found in the documentation.

The assisted factory extension has a very simple API with the annotation @ContributesAssistedFactory. This annotation requires two parameters, the anvil scope and the assisted factory interface to bind.

@Inject
@ContributesAssistedFactory(
  scope = AppScope::class,
  assistedFactory = MoviesBloc.Factory::class
)
class RealMoviesBloc(
  @Assisted context: ComponentContext
  private val repository: MovieRepository,
): MoviesBloc, ComponentContext by context {
  //
}

Then inject the assisted factory interface wherever needed.

@Inject
@ContributesAssistedFactory(
  scope = AppScope::class,
  boundType = RootBloc.Factory::class
)
class RealRootBloc(
  @Assisted context: ComponentContext,
  private val moviesBlocFactory: MoviesBloc.Factory,
): RootBloc, ComponentContext by context {

  private fun createMoviesBloc(context: ComponentContext) = 
    moviesBlocFactory.create(context)
}

Warning

The @Assisted parameters order of the real implementation constructor must be the same order as your assisted factory interface. If two of the same type are used, these will be mixed up when generating the default factory implementation with the extension if not passed in the exact same order. Consider the following example:

interface FooFactory {
  fun create(id: String, config: String): Foo
}

Success

@Inject
@ContributesAssistedFactory(
  scope = AppScope::class,
  assistedFactory = FooFactory::class,
)
class RealFoo(
  @Assisted private val id: String,
  @Assisted private val config: String,
): Foo

Failure

@Inject
@ContributesAssistedFactory(
  scope = AppScope::class,
  assistedFactory = FooFactory::class,
)
class RealFoo(
  @Assisted private val config: String,
  @Assisted private val id: String,
): Foo

Resources