Rethinking Android Dependency Management With Koin

Android dependency management with Koin

Rethinking Android dependency management with Koin

Every serious Android 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 of its benefits, also has a lot of quirks which sometimes make the job more complex and difficult, which 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.

Koin jumps in to save the day

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.

Koin is a Kotlin-based dependency management framework, which heavily relies on its language features like extension functionsdelegateslambdas, 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.

Jumping into Koin

To add Koin to your 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

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 necesary.

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.

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()

Injecting a dependency

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!

The inner works

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 a 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.

Making complex features simple

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 setup 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! :]

Attaching scopes to the Android lifecycle

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.

Sending dynamic parameters to providers

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 ContextFragmentManager 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.