Integrate Kotlin-Inject-Anvil To Tv Maniac
Integrate Kotlin-Inject-Anvil To Tv Maniac 관련
Intro
If you've used Anvil before, you know it takes away a lot of the boilerplate code and makes DI seamless. If Anvil is new to you, it basically allows you to contribute dagger modules and component interfaces to your DI graph, merge all the contributions, and add them to your component during compilation. Ralf Wonderatschek and Gabriel Peal gave an in-depth talk about this. Dagger + Anvil: Learning to Love Dependency Injection. You should check it out.
I have been using evant/kotlin-inject
on my pet project (thomaskioko/tv-maniac
) for a while now, and I have had a good time with it, coming from using Dagger in other projects. One thing I missed was using Anvil. This was not available until recently, when amzn/kotlin-inject-anvil
joined the chat.
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.
- Going Modular — The Kotlin Multiplatform Way
- KMM Preferences Datastore (
_thomaskioko
) - KMP Environment Variables (Part 1) (
_thomaskioko
) - Intercepting Ktor Network Responses in Kotlin Multiplatform (
proandroiddev
) - Navigating the Waters of Kotlin Multiplatform: Exploring Navigation Solutions (
proandroiddev
) - Enhancing iOS UI Previews: Swift UI Packages & Kotlin Multiplatform Mobile (
_thomaskioko
). - Integrate Kotlin-Inject-Anvil To Tv Maniac — You are here**.** 👈
If you want to see the code, here's the pull request (thomaskioko/tv-maniac
).
Koltlin-Inject-Anvil Integration
Before integrating amzn/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.
Add kotin-inject-anvil
Dependencies
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)
Add @ContributesTo
Annotation
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.
interface CastComponent {
@Provides
@ApplicationScope
fun provideCastDao(bind: DefaultCastDao): CastDao = bind
@Provides
@ApplicationScope
fun provideCastRepository(bind: DefaultCastRepository): CastRepository = bind
}
@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.
Add @ContributesBinding
Annotation
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.
@Inject
class DefaultCastRepository(
private val dao: CastDao,
) : CastRepository {
// ...
}
@Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultCastRepository(
private val dao: CastDao,
) : CastRepository {
// ...
}
Add KSP Dependencies
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
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
}
Delete Manual Bindings
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. 🥳
Final Boss: @MergeComponent
Annotation
@ContributesSubcomponent
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.
@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: RootPresenter
companion object
}
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. (amzn/kotlin-inject-anvil
)
@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
}
}
@MergeComponent
Annotation
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
.
@Component
@SingleIn(AppScope::class)
abstract class ApplicationComponent(
@get:Provides val application: Application,
) : SharedComponent() {
abstract val initializers: AppInitializers
companion object
}
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
}
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.
Conclusion
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 (vRallev
) and all the contributors have done a fantastic job with amzn/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 (vRallev
), for reviewing the article. Until we meet again, folks. Happy coding! ✌️
References
Info
This article is previously published on proandroiddev
)