Dear Dagger: I’ve found someone new
Koin is a dependency injection library that truly loves me
Dear Dagger,
We’ve had some great times together, and I’ll always hold you dear to my heart. You introduced me to a world in which I always get what I want. You helped me get what I needed before I even knew I needed it. You’ve helped me be more confident in myself.
But we’ve grown apart. You’re complicated. You’re difficult. You take up so much of my time. I’ve found another library that I feel a stronger connection with. They get me; I get them.
I’ll never forget you.
I have a confession to make: I don’t really understand how Dagger works. I’ve used it plenty, and reaped the benefits, but I’ve always struggled with it. It’s a complex beast, and I’ve never fully grasped it. Until now, I’ve been putting off implementing dependency injection in my Android project because I’ve been dreading trying to once more attempt to work out the best approach for the task. While Dagger is often praised for its flexibility, I didn’t want to have to spend hours deciding between the numerous implementation strategies for my small project — I don’t think I’ve ever read two blog posts which have implemented it the same way, and I wasn’t prepared to make that decision.
Koin operated
Being a Kotlin developer, I had heard of Koin before. I had always written it off as the less mature, less powerful alternative to Dagger. For my project, however, it revealed itself to me once more, and this time stood out as a potential solution to my problem. Koin sells itself as a “ pragmatic lightweight dependency injection framework” — I needed to know for myself just how pragmatic and lightweight it was.
Set up
Setting Koin up in an Android project is simple, and gives a good glimpse into its functional approach. All it requires is invoking the startKoin function in your App class and giving it a bunch of modules with which it can resolve dependencies.
startKoin {
androidContext(this@App)
modules(listOf(
databaseModule,
mediaManagerModule,
addItemModule
))
}You might choose to combine all your globally-scoped dependencies into a single appModule; my project uses features packages, so I wanted to have one module per feature — for example, the ../feature/additem/model/AddItemRepository class is injected by the ../feature/additem/di/AddItemModule.
What do the modules look like, you may ask? Well, that’s simple too!
val mediaManagerModule = module {
single {
MediaManager()
}
}That’s all that’s required for a globally-scoped singleton — Koin will now be able to provide you with a reference to the MediaManager wherever you need it. Looking good so far!
Scoped dependencies
If you’re using Jetpack’s Architecture Components, your ViewModels will (usually) have a one-to-one relationship with your Fragments or Activities, so it makes sense to scope them appropriately. Each of my features has its own Repository which provides access to the database, so this should be similarly scoped. Additionally, both the ViewModel and the Repository will have their own dependencies — the Repository is a dependency of the ViewModel, and the ViewModel also needs reference to the MediaManager singleton. Additionally, the Repository needs a reference to the appropriate DAOs from the Database singleton.
How do we represent this using Koin? Easy!
val addItemModule = module {
scope(named<AddItemFragment>()) {
scoped {
AddItemRepository(
get<Database>().itemDao()
)
}
viewModel {
AddItemViewModel(get(), get())
}
}
}So what’s going on here? Simple: we’ve created a scope which is named using the Fragment that the scope surrounds. Inside the scope, we’ve declared two things: a scoped singleton dependency of AddItemRepository, and a viewModel dependency of AddItemViewModel (which tells Koin to provide the AddItemViewModel using Jetpack’s ViewModelProviders static functions).
You might be wondering what the get() function does here. get() is the glue that helps us stick dependencies together by using Koin to inject dependencies that were declared either in this Module or a different one. When we constructed the AddItemRepository above, we told Koin to give us our Database dependency so that we can provide the approriate DAO; when we constructed the ViewModel, we told Koin to provide us with the AddItemRepository we just declared as well as the MediaManager singleton we declared in our previous globally-scoped module. Thanks to Kotlin’s generic type inference, we don’t need to specify the type returned by each call of the get() function, but note that each invocation returns a different type (see the declaration of the AddItemViewModel constructor below for clarification).
And that’s all we need to do to provide AddItemFragment its dependencies. If we wanted to share these dependencies across multiple Fragments, all we would have to do is pass a String into the non-generic overload of the named function — for example, named("USER_SESSION") — and retrieve the dependencies from that scope in the appropriate Fragments using getKoin().getScope(name).
Injection
Before setting up dependency injection, our AddItemFragment looked something like this:
class AddItemFragment : Fragment() {
private lateinit var viewModel: AddItemViewModel
override fun onActivityCreated(savedInstanceState: Bundle?) {
viewModel = ViewModelProviders.of(this)
.get(AddItemViewModel::class.java)
}}
And our AddItemViewModel looked something like this:
class AddItemViewModel(app: Application) : AndroidViewModel(app) {
private val repository = AddItemRepository(
Databse.getDatabase(application).itemDao()
)
private val mediaManager: MediaManager = MediaManager()
}Good luck mocking out those dependencies for unit testing purposes!
OK, so let’s see how things look with our dependencies injected:
class AddItemFragment : Fragment() {
private val viewModel
by currentScope.viewModel<AddItemViewModel>(this)
}Remember that we scoped our ViewModel to its corresponding Fragment in our Module, so we need to resolve it by using the correct scope. Koin provides us a nice shortcut to do this with — currentScope will return the scope that was named<AddItemFragment>. Nice.
And the ViewModel?
class AddItemViewModel(
private val repository: AddItemRepository,
private val mediaManager: MediaManager
) : ViewModel()Lovely. Our dependencies have now been provided for us in the constructor, which means we can easily provide mocks instead when unit testing. What’s more, things are scoped appropriately, enforcing good engineering practice: AddItemRepository is only accessible from the “add item” context, while the MediaManager — a helper class for handling photo and video files — is a singleton shared across the app.
Closing thoughts
Time will tell how Koin scales in my project, but I’m extremely happy with my initial experience. It has provided a clean and easy implementation of dependency injection, and it was fast to learn and implement — even faster than it took me to write this post, in fact! And while I previously understood the concepts behind dependency injection, Koin has helped to demystify the implementation details which so often prevented me from loving Dagger unconditionally.
If you find yourself getting lost in the weeds with Dagger, give Koin a go. Its simplicity may just help you get over that hump.
If you enjoyed this post, don’t forget to 👏, and then follow me on Twitter @hndmrsh.
I’m also available for work, so connect with me on LinkedIn too!
Thanks to /u/Zhuinden for pointing out that the ViewModels no longer have to subclass AndroidViewModel.
