Skip to content

Assisted Factory Extension

An extension to help bind factory interfaces in anvil when using assisted injection from kotlin-inject.

Setup

The library is available through maven central, so include this in your repositories of your settings.gradle.kts.

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}
  • compiler - Maven Central
  • runtime - Maven Central

When using version catalogs, add the following version and library modules.

[versions]
kotlinInjectAnvilExtensions = "{version}"

[libraries]
kotlinInjectAnvilExtensions-assistedFactory-compiler = { module = "com.plusmobileapps.kotlin-inject-anvil-extensions:assisted-factory-compiler", version.ref = "kotlinInjectAnvilExtensions" }
kotlinInjectAnvilExtensions-assistedFactory-runtime = { module = "com.plusmobileapps.kotlin-inject-anvil-extensions:assisted-factory-runtime", version.ref = "kotlinInjectAnvilExtensions" }

Then in the build.gradle.kts configure ksp with the libraries.

dependencies {
    // update with your app's targets.
    val targets = listOf(
        "kspAndroid",
        "kspIosX64",
        "kspIosArm64",
        "kspIosSimulatorArm64"
    )
    commonMainImplementation(libs.kotlinInjectAnvilExtensions.assistedFactory.runtime)
    targets.forEach {
        add(it, libs.kotlinInjectAnvilExtensions.assistedFactory.compiler)
    }
}

Why Assisted Factory?

The current APIs between kotlin-inject and kotlin-inject-anvil do allow you to use assisted injection, however to bind a factory interface in your dependency graph to generate an assisted dependency can require a bit of boiler plate. For this example let's assume the following dependency:

interface MovieRepository {
    fun get(): Movie
}

@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RealMovieRepository(@Assisted val id: String) : MovieRepository {
    override fun get(): Moview = TODO()
}

The above code will bind a factory method (String) -> RealMovieRepository that can be injected in any other dependency. Although for testing purposes, it would be better if the factory method bound was returning the interface instead like (String) -> MovieRepository. An even better solution would be to create a strongly typed interface and bind that to the real factory method, however this requires some boiler plate creating a real factory that injects the real factory and implementing the factory interface.

interface MovieRepository {
    interface Factory {
        fun create(id: String): MovieRepository
    }
}

@Inject
@ContributesBinding(AppScope::class)
@SingleIn(AppScope::class)
class RealMovieRepositoryFactory(
    val realFactory: (String) -> RealMovieRepository,
) : MovieRepository.Factory {

    override fun create(id: String): MovieRepository = realFactory(id)
}

This is where the assisted factory extension comes in by implementing that boiler plate for you removing the need to implement a real factory binding.

Usage

To use the assisted factory anvil extension, all that is needed is to annotate the dependency with the @ContributesAssistedFactory annotation.

interface MovieRepository {
    fun get(): Movie

    interface Factory {
        fun create(id: String): MovieRepository
    }
}

@Inject
@ContributesAssistedFactory(
    scope = AppScope::class,
    assistedFactory = MovieRepository.Factory::class,
)
class RealMovieRepository(
    @Assisted val id: String,
) : MovieRepository {
    override fun get(): Movie = TODO()
}

The above code will generate a real factory and bind the factory interface in the dependency graph for you. Then you can simply inject that factory into any other class.

@Inject
class MovieDetailViewModel(
    private val factory: MovieRepository.Factory
) {
    fun create(id: String) {
        val repository: MovieRepository = factory.create(id)
    }
}