Rethinking Android dependency Management With Koin
Filip Babic
Posted on January 31, 2019
Every serious project should intently think about its structure, architecture, and management of dependencies within the project features and modules. In the world of Android, Dagger is definitely the most popular framework and tool every developer uses for this purpose. But Dagger, with all its benefits, also has a lot of quirks which sometimes make the job more complex and difficult, and it could also introduce a pile of boilerplate code, just to set up.
Not only that, but Dagger setup tends to require a lot of fine-grained knowledge. The documentation itself goes on for miles, and unless you have a lot of experience with it, you may find yourself in trouble, when a Gradle build error pops up, and it doesn’t tell you much. Also, since Dagger works on the principle of annotation processing and code generation, project builds are noticeably slower.
Because of this, developers have been searching for a new tool which would help them manage dependencies in projects. With the arrival of Kotlin, and a plethora of new and useful language features, frameworks like Koin emerged.
Our savior, Koin!
Koin is a Kotlin-based dependency management framework, which heavily relies on its language features like extension functions, delegates, lambdas, and generics. It seeks to serve you the same toolset Dagger holds, but without all the high-level requirements to learn it, set it up or maintain. Furthermore, it greatly reduces the amount of code needed to set up your dependency graph, while at the same time keeping readability and simplicity as its main traits.
One of the key differences between Koin and Dagger is that Koin works within the runtime scope of an application. This means it doesn’t generate any code, to begin with, it doesn’t have to do any annotation processing, or passes throughout the project files, in turn greatly reducing build time, as compared to Dagger.
To add Koin to you project, you have to add some of these dependencies to your gradle files:
compile “org.koin:koin-core:$version”
compile "org.koin:koin-android:$version"
compile "org.koin:koin-android-scope:$version"
compile "org.koin:koin-android-viewmodel:$version" //Architecture components
After that, you're ready to build your DI! A sample module definition would look like this:
val userModule = module {
single { get().create(UserApiService::class.java) }
single { UserRepositoryImpl(userApiService = get()) as UserRepository }
factory { UserPresenter(userRepository = get()) as UserContract.Presenter } }
In order to create a module, with our provider functions, all we have to do is call the module global function from Koin. From within the lambda block, we can define any number of providers, in various ways. If we, for example, declare that our ApiService is being built within the single function, it will be created as a lazy Singleton object, granting us both the memory optimization of possibly not using the dependency (hence lazy allocation), but also of reusing if necessary.
On the other hand, if we declare the Presenter within a standard provider — the factory function, every time we want to receive an instance, we’ll get a completely new one. Just like a regular factory. You might have also noticed the get function calls. Any time you need a dependency, all you have to do is call get(), and Koin, with the usage of reified generic parameters, will find its declaration within the graph. By using get, you can receive the dependency beforehand, and set it up before injecting, if you need some kind of initialization.
Injecting dependencies
To receive the value in your code — inject the dependency, all you have to do is the following:
private val userPresenter: UserContract.Presenter by inject()
You simply declare the field, with the type you need injected, and you let it be injected by a delegate. Kotlin is just awesome, isn’t it!
To explain how all this works, you have to imagine the simplest way of storing provider functions within the app — a Map . Koin effectively takes in all your ModuleDefinition functions and looks at the types they are returning. Once that’s done, it stores all of the providers in a map, where each type is a map key, which returns the provider function, as you defined it.
The whole API and inner implementation is a wee bit more complicated than that, but that’s the gist of it. As such, when you call the get function somewhere in your application, it looks for the type you’re trying to receive, and if that type is defined, it uses the provider function to return the instance. If there isn’t one, Koin throws an exception, saying the definition couldn’t be found.
Since Koin works in the runtime, it cannot do static analysis, like Dagger does with its annotations, to see if all of the required/used definitions are available. Because of that, it can only do checks when you launch the app, ultimately throwing exceptions if something is wrong.
We’re fairly sure you’ve managed to learn the Koin DSL in five minutes. And it’s pretty cool that Koin is so simple in nature, doesn’t take much time to learn or write, but that isn’t its biggest benefit. The most useful thing with Koin is that you’re able to set up elaborate mechanisms, without having to think much about the implementation itself. Some of these features are scoping mechanisms and dynamic parameters in provider functions.
Let’s hop onto the implementation of these hardcore features! :]
Scoping in Android
If you were to build scoping in Dagger, you’d have to provide specific annotations which dictate the scope each component is attached to. After that, you’d have to create and destroy components as you go, in certain Android lifecycle methods, to have dependencies cleared up, or filled, in the graph.
When using Koin, the first thing you have to do is declare that a dependency is built using the scopemodule definition function:
scope("USER_SCOPE") {
UserPresenter(userRepository = get()) as UserContract.Presenter }
Once you’ve done that, Koin knows that it needs to differentiate this provider from the others. When using the provided dependency, as long as your Android component (e.g. Fragment) is alive, the Presenter will be reused. Next, you have to initialize that scope within the Android component, by calling the `bindScope` function, and passing in the scope you want to bind:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bindScope(getOrCreateScope("USER_SCOPE"))
}
The last thing you have to do… Well, there isn’t the last thing, that’s pretty much it! :]
You don’t have to worry about the Android component, the module or the scope itself. Once you connect the two parts, they take care of themselves. After the Android component is destroyed — onDestroy
is called, the module is cleared up, and dependencies are released. You can build on top of this behavior, creating your own scope retainers, which would persist even through configuration changes, effectively optimizing how you rebuild your dependencies and fragments in those cases.
All of this works, since Koin embraces the LifecycleOwner
and LifecycleObserver
API from Android. It listens to certain events — like the onDestroy
or onCreate
using the observer interface, and as such can react to changes in due time.
Dynamic runtime parameters
To send parameters in runtime, dynamically, when you request a certain dependency is rather simple as well. In Dagger, you’d have to store this inside a factory of sorts, pass the dependency to a component you’re building, or a similar pattern you’d have to build yourself.
In Koin, you don’t have to recreate the modules or providers. Knowing that they work in the runtime, you can simply pass a list of parameters to the function you are retrieving the dependency from, like the inject function you saw before. Let’s say one of our dependencies, like the Presenter, requires a userId: String parameter in order to set up correctly. You can pass that userId as a parameter the following way:
private val userPresenter: UserContract.Presenter
by inject(parameters = { parametersOf("dj80a2k") })
And once you’ve sent a parameter, you can expect it within the provider, as the lambda argument it. The ParameterList is a class with the variable arguments as its only property, and as such, you can send any number of parameters, ultimately retrieve them in order:
scope("USER_SCOPE") {
val userId: String = parameters [0]
UserPresenter(userRepository = get(), userId = userId) as UserContract.Presenter
}
This makes it extremely simple to create and provide dependencies which require some Android component or dependency, like the Context, FragmentManager and similar.
Summing it up
There is so much that Koin provides and allows us to do. It’s one of those things that just work, do everything you need from it and yet there doesn’t seem to be a single downside. It almost sounds too good to be true. But it isn’t.
Koin is built in a very clean manner, the code behind is simplified to the extreme, relying on time-and-tested concepts and features. The makers of Koin also have plenty of ideas that they want to implement, improve upon or optimize in the future, which definitely makes it a thing you should try using if you’re looking to escape Dagger difficulties.
P.S.
Thanks for reading my post! Feel free to ask any questions and discuss Koin with me! :] Also, if you're looking for an Android opportunity, Five's recruiting)! You could join our awesome Android team, and participate in these creative events yourself. ^
Posted on January 31, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.