Introducing kotlin-inject-anvil

Posted on 2025-04-03

My team open-sourced the dependency injection framework kotlin-inject-anvil a few months ago. Dependency injection is a core piece of our and many mobile architectures in general as it removes coupling and enables easier testing. What I like about kotlin-inject-anvil in particular is that it’s a compile time dependency injection framework, meaning if you forget to provide a binding or have duplicate bindings, then you get an error at build time rather later at runtime in the app. This prevents you from shipping bugs to customers.

kotlin-inject-anvil removes a lot of the boilerplate you often see with other frameworks. All you have to do is to declare that a specific implementation should be used for a certain API, inject all required dependencies and the framework takes care of the rest. It couldn’t be simpler than that:

interface WeatherRepository {
    suspend fun provideForcast(): Forecast
}

@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class NetworkWeatherRepository(
    private val httpClient: HttpClient,
) : WeatherRepository {
    override suspend fun provideForecast(): Forecast {
        val jsonResponse = httpClient.sendRequest(..)
        return Json.decodeFromString<Forecast>(jsonResponse)
    }
}

Once you’re accustomed to the annotations, you’ll quickly stop thinking about common questions such as where your dependencies like the HttpClient are coming from, how the dependencies are implemented, where and when they’re instantiated, how to implement singletons, how to cache objects within a certain lifecycle, etc. Need another dependency? No problem, simply add another constructor parameter. These are problems the framework solves for you.

The best part about kotlin-inject-anvil is that it was purposefully built for Kotlin Multiplatform. It allows you to implement common APIs with platform specific dependencies. No expect / actual declarations nor custom wiring is needed. All you as consumer have to do is adding annotations to the implementation classes and the framework takes care of the rest:

// src/commonMain
interface WeatherRepository {
    suspend fun provideForcast(): Forecast
}

// src/androidMain
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class AndroidWeatherRepository(
    private val application: Application,
) : WeatherRepository { ... }

// src/iosMain
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class IosWeatherRepository(
    private val uiApplication: UIApplication,
) : WeatherRepository { ... }

Dependency injection keeps evolving and becomes simpler and more flexible in each iteration. The next step will be Metro, which combines many of the learnings of previous frameworks and adds a deeper integration with the Kotlin compiler and IDE. I’ll summarize this evolution and how kotlin-inject-anvil helps us in practice at my KotlinConf 2025 talk.