Pragmatic Koin crash course: my two cents

Dependency injection (DI) is a widely spread concept that is probably used in most modern big software projects.

There are plenty of DI frameworks in the Kotlin/Java/Android world. Dagger, maybe the most popular in the Android community, is extremely powerful and fast but this brings along complexity. For smaller projects, that DI is needed mainly to be able to replace things easily in tests, I recently discovered Koin. It's popular among the Kotlin community, fast, and easy to pick up.

Comparing the 2 DI libraries is outside of the scope of this article. I am pretty sure Koin can be used in big complicated applications. What I want to document here is a pragmatic approach in getting started with Koin in your application, and in your tests.

Modules

The DI library needs to know how to create the instances that you require. This is the purpose of modules.

val appModule = module {
    single { SharedPreferencesStorage() }
    factory { CurrencyController() }
}
  • single indicates that every time SharedPreferencesStorage is requested, the same instance will be returned.
  • factory means that a new instance will be created every time a CurrencyController is requested.

Start Koin

Now that you have created your modules, you need to initiate Koin. Call this a single time, e.g. in your Application's onCreate() in Android.

startKoin {
    modules(appModule)

    if (BuildConfig.DEBUG) {
        androidLogger()
    }
    androidContext(this@MyApplication)
}

You can see that there are some specific convenience things you can provide for each "platform" (Android, Java, Ktor). In this example, we are providing a logger only for debug builds, and the application Context.

Inject

"Injection" is how you actually get instances of your classes when using a DI.

  • Use by inject delegate method
private val currencyController by getKoin().inject<CurrencyController>()
  • Use get() method
private val storage = getKoin().get<SharedPreferencesStorage>()

To use the getKoin() method, your class should implement the KoinComponent interface. In Android, some common system classes, such as Activity and Fragment, already implement this interface (by extension functions) so you don't have to do anything.

Note: You can use the get() method in Modules. For instance, if CurrencyController requires a SharedPreferencesStorage to be initialized:

class CurrencyController(private val storage: SharedPreferencesStorage) {
    [...]
}

[...]

val appModule = module {
    single { SharedPreferencesStorage() }
    factory { CurrencyController(get()) }
}

Test

Using a DI library makes it easy replacing real implementations for fakes/mocks in your tests. Koin makes testing easy by providing convenient ways to replace real implementations with mocks (in this example in combinations with Mockito).

class CurrencyControllerTest : KoinTest {

    @get:Rule
    val koinTestRule = KoinTestRule.create {
        modules(
            module {
                single { CurrencyController(get()) }
            }
        )
    }

    @get:Rule
    val mockProvider = MockProviderRule.create { clazz ->
        Mockito.mock(clazz.java)
    }
    
    // Under test
    private val currencyController : by inject<CurrencyController>()
    
    @Test
    fun testSomething() {
        declareMock<SharedPreferencesStorage> {
            given(this.loadSomething(any())).willReturn("Hello!")
        }
        assertEquals(currencyController.myMethodThatUsesStorage(), false)
    }    
    
    [...]
 }

Of course, there are more features (e.g. proving Android's ViewModels, scopes, binding implementations to interfaces, etc) and there's a link in each section to point you to the official doc. The fact that you can write a crash course for Koin in 500 words, shows how simple and quick is to get started.