Insert Koin in your apps, simplify dependency injection in Kotlin

Koin is a lightweight dependency injector for Kotlin developers, written in pure Kotlin. Although, it can works with Spark, Ktor or simply with Kotlin applications, I will focus on Android in this post.

Why Koin and not other dependency injectors?

I always wanted to learn dependency injection with Dagger2, but it was very difficult to understand and its documentation is tedious. Some developers who wrote its own service locator instead of using Dagger. I was one of them.

Koin advantages can be summarised in:

  • Simplicity. Developers without knowledge in Dagger can inject dependencies in a few steps.
  • Usage of functional resolution only: no proxy, no code generation, no reflection.
  • Support for Android, AndroidX and Android Architecture Components what makes more attractive than others such as Kodein

Let’s start!

How to include koin in our Android app?

Firstly, include the dependency in the app build.gradle

// koin
implementation "org.koin:koin-core:$koin_version"

Secondly, override your application instance to start koin with a list of modules that define our dependencies.

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        startKoin(this, listOf(applicationModule))
    }
}

That’s it! We have koin working in our application.

How to specify a koin module?

Koin is a DSL and modules are defined by these keywords:

  • single: create a single instance component (unique instance, singleton).
  • factory: create a factory instance component (new instance on each demand).
  • androidContext: resolve an Android context.
  • applicationContext:resolve the application context.
  • get: retrieve the needed component.

Imagine, we have an application that lists your nearby shops. In this application, we have a repository that needs a local and remote source data.

interface ShopRepository {
    suspend fun getShops(): Resource<Throwable, List<Shop>>
}

class ShopRepositoryImpl(
        private val remoteShopsDataSource: RemoteShopsDataSource,
        private val localShopsSourceData: LocalShopsSourceData
): ShopRepository {
....
}

Our application module will be as simple as:

val applicationModule =  module {
    ...

    // Local source data
    single { LocalShopsSourceDataImpl() as LocalShopsSourceData }

    // Remote source data
    factory { RemoteShopsDataSourceImpl() as RemoteShopsDataSource }

    // Repositories
    factory { ShopRepositoryImpl(get(), get()) as ShopRepository }

   ...
}

You don’t need to obsess with the dependency injection. It’s so simply…!

I work with Android Architecture Components, I don’t know if koin resolves dependency like ViewModels…

No worries. It provides another artifact for this case. In our app build.gradle, include also the koin-androidx-viewmodel artifact.

implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"

Following with our example of nearby shops, we want to list my nearby shops in an activity using a ViewModel.

Our first step is to include the dependency in the module using viewModel keyword.

val applicationModule =  module {
    ...

     // Local source data
     ...
    single { LocalShopsSourceDataImpl() as LocalShopsSourceData }

     // Remote source data
     ...
    factory { RemoteShopsDataSourceImpl() as RemoteShopsDataSource }

     // Repositories
    ...
    factory { ShopRepositoryImpl(get(), get()) as ShopRepository }

    // Use cases
   ...
    factory { GetShopsUseCase() }

    // View models
   ...
   viewModel { ShopsViewModel(get()) }

   ...
}

And then, in the activity or fragment, we need to retrieve it.

class ShopsActivity : AppCompatActivity() {

    private val viewModel by viewModel<ShopsViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_shop_listing)
       ...
    }
...
}

Also, we can get the ShopsViewModel instance using getViewModel() function:

class ShopsActivity : AppCompatActivity() {

    private lateinit var viewModel: ShopsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_shop_listing)
        viewModel = getViewModel()
       ...
    }
...
}

If we are using a shared ViewModel, just replace viewModel keyword by sharedViewModel or getViewModel() function by getSharedViewModel().

I need to pass a dependency dynamically, can I?

Yes, you can. Imagine the case you need to set a shop identifier to our ShopDetailsViewModel. In this case, we need to use the getProperty and setProperty keywords.

Having this view model,

class ShopDetailsViewModel(
        private val shopId: String,
        private val getShopDetailsUseCase: GetShopDetailsUseCase
): ViewModel() {

    companion object {
        private val KEY_ID = "KEY_ID"
    }
...
}

First, we need to declare our view model in the application module and define the property we want to get later.

val applicationModule =  module {
    ...

    // Local source data
    ...
    single { LocalShopsSourceDataImpl() as LocalShopsSourceData }

    // Remote source data
   ...
    factory { RemoteShopsDataSourceImpl() as RemoteShopsDataSource }

    // Repositories
    ...
    factory { ShopRepositoryImpl(get(), get()) as ShopRepository }

    // Use cases
   ...
    factory { GetShopDetailsUseCase() }

    // View models
   ...
    viewModel { ShopDetailsViewModel(getProperty(ShopDetailsViewModel.KEY_ID), get()) }

   ...
}

And then, before getting the view model, we need to set the dynamic parameter. In our case, the shopId that comes from an intent.

class ShopDetailsActivity : AppCompatActivity() {

   private lateinit var viewModel: ShopDetailsViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_shop_details)
        val shopId = // get shopId from intent
        setProperty(ShopDetailsViewModel.KEY_ID, shopId)
       viewModel = getViewModel()
       ...
    }
...
}

Okey, it’s great for a MVVM architecture, but what about MVP architectures?

If your application has a MVP architecture and you want a dependency scoped to an activity lifecycle, koin offers another artifact,koin-androidx-scope.

implementation "org.koin:koin-core:$koin_version"
implementation "org.koin:koin-androidx-scope:$koin_version"

In this case, we need to use the scope keyword in our application module and bindScope and getOrCreateScope methods in our Activity.

Regarding to our example, we will replace our ShopsViewModel by ShopsPresenter.

val applicationModule =  module {
    ...

     // Local source data
     ...
    single { LocalShopsSourceDataImpl() as LocalShopsSourceData }

     // Remote source data
     ...
    factory { RemoteShopsDataSourceImpl() as RemoteShopsDataSource }

     // Repositories
    ...
    factory { ShopRepositoryImpl(get(), get()) as ShopRepository }

    // Use cases
   ...
    factory { GetShopsUseCase() }

    // Presenters
   ...
   scope(ShopsActivity.SCOPE_ID) { ShopsPresenter(get()) }

   ...
}

And then, in the activity or fragment, we need to retrieve it.

class ShopsActivity : AppCompatActivity() {

    companion object {
        const val SCOPE_ID = "SCOPE_ID"
    }

    private val presenter: ShopsPresenter by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_shop_listing)

       bindScope(getOrCreateScope(SCOPE_ID))
       ...
    }
...
}

Testing

For both unit tests and instrumented tests, we have to include koin-test artifact.

testImplementation "org.koin:koin-test:$koin_version" // for unit tests
androidTestImplementation "org.koin:koin-test:$koin_version" // for instrumented tests

Once we have included these dependencies, extend your test class from KoinTest and call startKoin and stopKoin before and after each test.

class ShopRepositoryTest: KoinTest {
    
    @Before
    fun setUp() {
        startKoin(listOf(testingModule))
       ...
    }

    @After
    fun stop() {
        ...
        stopKoin()
    }

    @Test
    fun `test get shops - ok`() {
    ...
    }
...
}

There are cases in which we have to override one module for another one or, simply, we want to replace a dependency. Koin takes into account this option with the parameter overriding a module or a definition.

For example, we want to override our RemoteShopsDataSource by a mocked one.

module  {
    ...
    factory(override = true) { MockRemoteShopsSourceData() as RemoteShopsDataSource }
    ...
}

After doing this, MockRemoteShopsSourceData replaces to the defined RemoteShopsDataSourceImpl instance.

Order matters when listing modules and overriding definitions. You must have your overriding definitions in last of your module list.

If you have already started the injection and want to replace a module or definition, instead of startKoin for your tests, use  loadKoinModules function.

TL;DR

IMHO, as well as Kotlin has been a great step for Android developers, koin helps us to reduce very much dependency injection cost.

Insert Koin in your apps!

Related links

https://insert-koin.io/docs/1.0/documentation/

JitPack – Forget about cloning public repositories on Android

Clean Architecture design in NodeJS

 

 

Leave a Comment

Responsable » Solidgear.
Finalidad » Gestionar los comentarios.
Legitimación » Tu consentimiento.
Destinatarios » Los datos que me facilitas estarán ubicados en los servidores SolidgearGroup dentro de la UE.
Derechos » Podrás ejercer tus derechos, entre otros, a acceder, rectificar, limitar y suprimir tus datos.

By completing the form you agree to the Privacy Policy

This site uses Akismet to reduce spam. Learn how your comment data is processed.