This is a blog of an ongoing series on my journey with Kotlin Multiplatform. This article will focus on my experience and journey integrating/migrating to kotlin-inject-anvil into the project.
If you want to see the code, here’s the pull request.
Before integrating kotlin-inject-anvil, one thing that bothered me was how to approach the integration/migration. I thought the process would be a pain as I already have multiple modules in my project. Do I rip the bandaid off and do it all at once? Is it possible to do it gradually? Spoiler alert: it is possible to do it gradually. This approach might not work for your project, depending on the size of the team. There are multiple ways of doing this, but this worked for me. This approach made it easier to determine if I broke the current implementation or introduced new errors.
Here’s a quick overview of how I approached the migration.
- Add dependencies
- Apply
@ContributesTo
annotation - Apply
@ContributesBinding
annotation - Add ksp kotlin-inject-anvil compiler dependencies.
- Delete component interfaces.
- Replace
@Component
with@MergeComponent
and create a subcomponent.
Let’s take a quick look at how each step is implemented.
This is pretty straightforward. We need to add the dependencies to our project.
kotlinInject-anvil-compiler = { group = "software.amazon.lastmile.kotlin.inject.anvil", name = "compiler", version.ref = "kotlin-inject-anvil" }
kotlinInject-anvil-runtime = { group = "software.amazon.lastmile.kotlin.inject.anvil", name = "runtime", version.ref = "kotlin-inject-anvil" }
kotlinInject-anvil-runtime-optional = { group = "software.amazon.lastmile.kotlin.inject.anvil", name = "runtime-optional", version.ref = "kotlin-inject-anvil" }
kotlinInject-anvil-runtime-optional
is optional, and your project would work without it. I added it so I can get rid of my custom scope and use kotlin-inject-anvil’s scopes to keep everything consistent.
To make things easier, I created a bundle with kotlin-inject dependencies, and I use that instead.
[bundles]
kotlinInject = [
"kotlinInject-runtime",
"kotlinInject-anvil-runtime",
"kotlinInject-anvil-runtime-optional"
]
We can then add it to our module like so. implementation(libs.bundles.kotlinInject)
We can now annotate our interface components with @ContributesTo
. I also replaced my custom scope with kotlin-inject-anvil scope: @ApplicationScope
-> @SingleIn(AppScope::class)
. As mentioned, this is optional and will work with your custom scopes. Here’s how the component looks.
Before
interface CastComponent {@Provides
@ApplicationScope
fun provideCastDao(bind: DefaultCastDao): CastDao = bind
@Provides
@ApplicationScope
fun provideCastRepository(bind: DefaultCastRepository): CastRepository = bind
}
After
@ContributesTo(AppScope::class)
interface CastComponent {@Provides
@SingleIn(AppScope::class)
fun provideCastDao(bind: DefaultCastDao): CastDao = bind
@Provides
@SingleIn(AppScope::class)
fun provideCastRepository(bind: DefaultCastRepository): CastRepository = bind
}
One small thing I did later was move the @SingleIn
annotation to the class instead of having it in the binding functions.
The next thing we can do is annotate all classes that have interface implementations with @ContributesBinding
. Once we’ve plugged everything in, Anvil will provide the bindings for us, and we can get rid of the component above with the manual binding.
Before
@Inject
class DefaultCastRepository(
private val dao: CastDao,
) : CastRepository {
...
}
After
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultCastRepository(
private val dao: CastDao,
) : CastRepository {
...
}
To check if the changes we’ve made work as intended, we can add the Kotlin inject Anvil compiler dependency and generate the component classes. addKspDependencyForAllTargets(libs.kotlinInject.anvil.compiler)
. addKspDependencyForAllTargets
is an extension function that creates KSP configurations for each target. e.g kspAndroid
kspIosArm64
We can build our app and take a look at the generated code.
Anvil will generate the bindings for us similarly to what we had above. This will be generated for all our classes annotated with @ContributesBinding(AppScope::class)
.
@Origin(value = DefaultCastRepository::class)
public interface ComThomaskiokoTvmaniacDataCastImplementationDefaultCastRepository {@Provides
public
fun provideDefaultCastRepositoryCastRepository(defaultCastRepository: DefaultCastRepository):
CastRepository = defaultCastRepository
}
Now that our bindings and components are being generated, we can delete our component interfaces with provider functions.
In my previous implementation, each module was responsible for creating its own DI component. The shared module then added all these SuperType Components to the parent/final component for each platform component. This is a bit painful and can easily get out of hand as your project grows. 😮💨
Thanks to kotlin-inject-anvil, we can get rid of these as they are now generated for us once we add the merge annotation. 🥳
Since we can only have one component annotated with @MergeComponent
, we need to annotate ActivityComponent
to @ContributesSubcomponent
, create a factory that our parent scope will implement.
Before
@SingleIn(ActivityScope::class)
@Component
abstract class ActivityComponent(
@get:Provides val activity: ComponentActivity,
@get:Provides val componentContext: ComponentContext = activity.defaultComponentContext(),
@Component
val applicationComponent: ApplicationComponent =
ApplicationComponent.create(activity.application),
) : NavigatorComponent, TraktAuthAndroidComponent {
abstract val traktAuthManager: TraktAuthManager
abstract val rootPresenter: RootPresentercompanion object
}
After
You should note that we converted our abstract class to an interface, as only interfaces can be annotated with contributed @ContributesSubcomponent
. For more details on annotation usage and behavior, see the documentation.
@ContributesSubcomponent(ActivityScope::class)
@SingleIn(ActivityScope::class)
interface ActivityComponent {@Provides
fun provideComponentContext(
activity: ComponentActivity
): ComponentContext = activity.defaultComponentContext()
val traktAuthManager: TraktAuthManager
val rootPresenter: RootPresenter
@ContributesSubcomponent.Factory(AppScope::class)
interface Factory {
fun createComponent(
activity: ComponentActivity
): ActivityComponent
}
}
To create our graph and our components to our graph, we need to replace kotlin-injects
@Component
with kotlin-inject-anvil
@MergeComponent
and get rid of the SharedComponent
.
Before
@Component
@SingleIn(AppScope::class)
abstract class ApplicationComponent(
@get:Provides val application: Application,
) : SharedComponent() {
abstract val initializers: AppInitializers
companion object
}
After
I added annotation, removed the supertype from the application component, and added ActivityComponent.Factory
.
@MergeComponent(AppScope::class)
@SingleIn(AppScope::class)
abstract class ApplicationComponent(
@get:Provides val application: Application,
) : ActivityComponent.Factory {
abstract val initializers: AppInitializers
abstract val activityComponentFactory: ActivityComponent.Factory
}
Now, if we look at the generated code, we can see that Anvil adds all the generated components to our graph when we compile the app.
If you forget to delete any provide functions, you will get the following error at compile time.
e: [ksp] Cannot provide: com.thomaskioko.tvmaniac.data.cast.api.CastDao
e: [ksp] as it is already provided
This is expected; you can track down the duplicate provide method and delete it.
With this in place, we have now gotten rid of manual bindings, replacing them with @ContributesTo
and @ContributesBinding
. We also deleted our god component class and got rid of a lot of boilerplate, thanks to Anvil.
@Ralf and all the contributors have done a fantastic job with kotlin-inject-anvil. The integration was smooth. I’m looking forward to how these libraries evolve. (Maybe it should be renamed to KiAnvil. Get it? You know, like Keanu, because of how lethal it feels? No? 😂 Don’t worry, I will see myself out.)
Thanks, @Ralf, for reviewing the article. Until we meet again, folks. Happy coding! ✌️