Dependency injection from zero to hero with Koin

Dependency injection from zero to hero with Koin

Koin (https://insert-koin.io) is a pragmatic dependency injection framework for Kotlin/Android developers. Let's see how it will make your life easier thanks to the power of Kotlin language: from basics to the setup of Android architecture's components, without forgetting the testing part... And of course, how to migrate your “old” Dagger project.

At the menu also, the presentation of the new 2.0 release and some advanced APIs!

(light/online version)

E02cd1d0fc5c51ac491b484a258a63a6?s=128

Arnaud GIULIANI

June 07, 2019
Tweet

Transcript

  1. Dependency injection from zero to hero with Koin Arnaud Giuliani

    @arnogiu Kotliners 2019
  2. The CoffeeMaker App ☕ * aka the thermosiphon dagger’s nightmare

    sample app
  3. Pump +pump() Thermosiphon Heater +off() +on() +isHot() ElectricHeater CoffeeMaker +brew()

  4. interface Heater { fun on() fun off() fun isHot() :

    Boolean }
  5. class ElectricHeater : Heater { var heating: Boolean = false

    override fun on() { println("~ ~ ~ heating ~ ~ ~") heating = true } override fun off() { heating = false } override fun isHot(): Boolean = heating }
  6. interface Pump { fun pump() }

  7. class Thermosiphon(val heater: Heater) : Pump{ override fun pump() {

    if (heater.isHot()){ println("=> => pumping => =>") } } }
  8. class CoffeeMaker(val pump: Pump, val heater: Heater) { fun brew()

    { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } }
  9. class CoffeeApp { val coffeeMaker: CoffeeMaker fun run() { coffeeMaker.brew()

    } } fun main(vararg args: String) { startKoin(listOf(coffeeMakerModule)) CoffeeApp().run() }
  10. Pump +pump() Thermosiphon Heater +off() +on() +isHot() ElectricHeater CoffeeMaker +brew()

    how to assemble it?
  11. Koin

  12. - Intuitive DSL to describe it - Lightweight container to

    run it - Simple API to use it
  13. repositories { jcenter() } dependencies { // Koin for Kotlin

    implementation ‘org.koin:koin-core:$version’ } Gradle Setup
  14. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater) val coffeeMakerModule = module { single { CoffeeMaker(get(),get()) } single<Pump>{ Thermosiphon(get()) } single<Heater> { ElectricHeater() } } * also multi modules
  15. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater) val coffeeMaker = module { single { CoffeeMaker(get(),get()) } } val coffeeStuffs = module { single<Pump>{ Thermosiphon(get()) } single<Heater> { ElectricHeater() } } * also multi modules
  16. Koin Component a class that can use the Koin container

  17. ✅ open access to Kotlin extensions: inject(), get() identify a

    class linked to the Koin API KoinComponent is an interface marker bootstrap & help integrate with runtime (Android…)
  18. class CoffeeApp : KoinComponent { val coffeeMaker: CoffeeMaker by inject()

    fun run() { coffeeMaker.brew() } } fun main(vararg args: String) { startKoin(listOf(coffeeMakerModule)) CoffeeApp().run() }
  19. startKoin to start the Koin container

  20. fun run() { coffeeMaker.brew() } } fun main(vararg args: String)

    { startKoin { modules(coffeeMakerModule) } CoffeeApp().run() }
  21. ~ ~ ~ heating ~ ~ ~ => => pumping

    => => [_]P coffee! [_]P
  22. Pump +pump() Thermosiphon Heater +off() +on() +isHot() ElectricHeater CoffeeMaker +brew()

    lazy!
  23. val coffeeMakerModule = module { single { CoffeeMaker(get(),lazy { get<Heater()

    }) } single<Pump>{ Thermosiphon(get()) } single<Heater> { ElectricHeater() } }
  24. class CoffeeMaker(private val pump: Pump, _heater: Lazy<Heater>) { val heater

    by _heater fun brew() { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } } * Lazy is a Kotlin type
  25. Let’s get back with Dagger ...

  26. @Module(includes = [(PumpModule::class)]) internal class DripCoffeeModule { @Provides @Singleton fun

    provideHeater(): Heater { return ElectricHeater() } }
  27. @Module abstract class PumpModule { @Binds abstract fun providePump(pump: Thermosiphon):

    Pump }
  28. @Singleton @Component(modules = [DripCoffeeModule::class]) interface CoffeeApp { fun maker(): CoffeeMaker

    }
  29. class Thermosiphon @Inject constructor(private val heater: Heater) : Pump{ override

    fun pump() { if (heater.isHot()){ println("=> => pumping => =>") } } }
  30. class CoffeeMaker @Inject constructor(private val pump: Pump, private val _heater:

    Lazy<Heater>) { val heater by lazy { _heater.get() } fun brew() { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } } * Lazy is a Dagger type
  31. Just migrate to Koin, what else?

  32. Pump +pump() Thermosiphon Heater +off() +on() +isHot() ElectricHeater CoffeeMaker +brew()

  33. val coffeeMakerModule = module { single<Heater> { ElectricHeater() } }

  34. class Thermosiphon @Inject constructor(private val heater: Heater) : Pump{ override

    fun pump() { if (heater.isHot()){ println("=> => pumping => =>") } } }
  35. class Thermosiphon : Pump, KoinComponent { private val heater: Heater

    by inject() override fun pump() { if (heater.isHot()){ println("=> => pumping => =>") } } }
  36. class CoffeeMaker @Inject constructor(private val pump: Pump, private val _heater:

    Lazy<Heater>) { val heater: Heater by lazy { _heater.get() } fun brew() { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } }
  37. class CoffeeMaker @Inject constructor(private val pump: Pump) : KoinComponent val

    heater: Heater by inject() fun brew() { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } }
  38. http://bit.ly/dagger2koin From Dagger to Koin, a step by step migration

    guide
  39. None
  40. Can’t create Android components directly

  41. Activity, Fragment, Service … are extended as KoinComponent! out of

    the box! ✅ Koin extensions for Android
  42. repositories { jcenter() } dependencies { // Koin for Android

    implementation ‘org.koin:koin-android:$version’ } Gradle Setup
  43. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    // Reference Android context in Koin startKoin { androidLogger() androidContext(this@MyApplication) modules(coffeeMakerModule) } } }
  44. class MyActivity : AppCompatActivity() { val coffeeMaker: CoffeeMaker by inject()

    }
  45. Get the Android context?

  46. class MyComponent(val context : Context) val myModule = module {

    single { MyComponent(androidContext()) } }
  47. class MyComponent(val context : Context) val myModule = module {

    single { MyComponent(get()) } }
  48. Dealing with transient components? ie. presenters …

  49. class MyPresenter val myModule = module { // will create

    MyPresenter on each call factory { MyPresenter() } } class MyActivity : AppCompatActivity() { }
  50. class MyPresenter val myModule = module { // will create

    MyPresenter on each call factory { MyPresenter() } } class MyActivity : AppCompatActivity() { // new instance on each call val presenter: MyPresenter by inject() }
  51. Ready for Android Architecture Components

  52. repositories { jcenter() } dependencies { // Koin for Android

    + ViewModel features implementation ‘org.koin:koin-android-viewmodel:$version’ } Gradle Setup
  53. ✅ Koin extensions for Android ViewModel

  54. class MyViewModel(val coffeeMaker : CoffeeMaker) : ViewModel() val coffeeMakerModule =

    module { viewModel { MyViewModel(get()) } } class MyActivity : AppCompatActivity() { // inject ViewModel val myViewModel: MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { // get ViewModel val myViewModel: MyViewModel = getViewModel() }
  55. //... viewModel { MyViewModel(get()) } } class MyActivity : AppCompatActivity()

    { // inject ViewModel val myViewModel: MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { // get ViewModel val myViewModel: MyViewModel = getViewModel() }
  56. //... viewModel { MyViewModel(get()) } } class MyFragment : Fragment()

    { // inject ViewModel val myViewModel: MyViewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { // get ViewModel val myViewModel: MyViewModel = getViewModel() }
  57. //... viewModel { MyViewModel(get()) } } class MyFragment : Fragment()

    { // inject ViewModel from parent activity val myViewModel: MyViewModel by sharedViewModel() overrid e fun onCreate(savedInstanceState: Bundle?) { // get ViewModel val myViewModel: MyViewModel = getViewModel() }
  58. //... viewModel { MyViewModel(get()) } } class MyFragment : Fragment()

    { // inject ViewModel from parent activity val myViewModel: MyViewModel by sharedViewModel() overrid e fun onCreate(savedInstanceState: Bundle?) { // get ViewModel val myViewModel: MyViewModel = getViewModel() } val viewModel = ViewModelProviders .of(this.activity) .get(MyViewModel::class.java)
  59. Testing easily with Koin ✅

  60. repositories { jcenter() } dependencies { // Koin for Tests

    testImplementation ‘org.koin:koin-test:$version’ } Gradle Setup
  61. Checking your modules ✔

  62. class ModuleTest { @Test fun `check all definitions from coffeeMakerModule`()

    { startKoin { modules(coffeeMakerModule) }.checkModules() } }
  63. ✅ Koin extensions with JUnit & Mockito

  64. class CoffeeMakerTest : KoinTest { val coffeeMaker: CoffeeMaker by inject()

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } declareMock<Heater>() given(heater.isHot()).will { true } coffeeMaker.brew() Mockito.verify(heater, times(1)).on() Mockito.verify(heater, times(1)).off() } }
  65. class CoffeeMakerTest : KoinTest { val coffeeMaker: CoffeeMaker by inject()

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } declareMock<Heater>() given(heater.isHot()).will { true } coffeeMaker.brew() Mockito.verify(heater, times(1)).on() Mockito.verify(heater, times(1)).off() } }
  66. class CoffeeMakerTest : KoinTest { val coffeeMaker: CoffeeMaker by inject()

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } declareMock<Heater>() given(heater.isHot()).will { true } coffeeMaker.brew() verify(heater, times(1)).on() verify(heater, times(1)).off() } }
  67. Upgrading to Koin 2.0

  68. Since first 1.0 version…

  69. Simple DSL module, single, factory, get

  70. Pragmatic API ✌ KoinComponent, get, inject

  71. ⚠ performances ⚠

  72. @Sloydev - 450 definitions benchmark

  73. None
  74. None
  75. * non linear startup / Kotlin overhead ** split modules

    startup if needed
  76. Benchmarks https://github.com/InsertKoinIO/thermosiphon_dagger2koin https://github.com/Sloy/android-dependency-injection-performance https://github.com/arnaudgiuliani/android-dependency-injection-performance

  77. Upgrade to Koin 2.0

  78. None
  79. None
  80. None
  81. Some API Break - Simplified module DSL (no more inner

    modules) - Revamped Scope API - Context isolation & startKoin DSL (koinApplication) - Definition resolution (default & qualifiers)
  82. http://bit.ly/ready4koin

  83. More about Koin✨

  84. - Container DSL - Qualifiers (string/type) - Global vs isolated

    context - Scope API - Android, Scopes & lifecycle - Injection Parameters - AndroidX - Koin for Java - Koin for Ktor
  85. What is a scope? A scope is a context with

    fixed duration of time, in which an object exists. When the scope ends, any objects bound under that scope cannot be injected again.
  86. Components’ lifetime Container Single Factory Scope Factory Factory

  87. Components Container Data Repository View User Session View View

  88. module { scope(named("Session")) { scoped { SessionData() } } }

    // create scope with id "session1" and qualifier "Session" val session1 = getKoin().createScope("session1",named("Session")) // get SessionData instance from scope "session1" val data : SessionData = session1.get() // close instances scope "session1" scope.close()
  89. module { scope(named("Session")) { scoped { SessionData() } } }

    // create scope with id "session1" and qualifier "Session" val session1 = getKoin().createScope("session1",named("Session")) // get SessionData instance from scope "session1" val data : SessionData = session1.get() // close instances scope "session1" session1.close()
  90. repositories { jcenter() } dependencies { // Koin for Android

    + Scope feature implementation ‘org.koin:koin-android-scope:$version’ } Gradle Setup
  91. ✅ Extensions for Android Lifecycle + Koin Scope

  92. module { scope(named<MyActivity>()) { scoped { MyPresenter() } } }

    class MyActivity : AppCompatActivity { val presenter : MyPresenter by currentScope.inject() } ** follow lifecycle * currentScope is provided by extension
  93. version = “2.0.1" repositories { jcenter() } dependencies { //

    Kotlin Core features implementation "org.koin:koin-core:$version" // Android basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features
  94. dependencies { // Kotlin Core features implementation "org.koin:koin-core:$version" // Android

    basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features implementation "org.koin:koin-android-viewmodel:$version" // Kotlin Checks + JUnit & Mock features testImplementation "org.koin:koin-test:$version" }
  95. dependencies { // Kotlin Core features implementation "org.koin:koin-core:$version" // Android

    basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features implementation "org.koin:koin-android-viewmodel:$version" // Kotlin Checks + JUnit & Mock features testImplementation "org.koin:koin-test:$version" }
  96. dependencies { // Kotlin Core features implementation "org.koin:koin-core:$version" // Android

    basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features implementation "org.koin:koin-android-viewmodel:$version" // Kotlin Checks + JUnit & Mock features testImplementation "org.koin:koin-test:$version" }
  97. dependencies { // Kotlin Core features implementation "org.koin:koin-core:$version" // Android

    basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features implementation "org.koin:koin-android-viewmodel:$version" // Kotlin Checks + JUnit & Mock features testImplementation "org.koin:koin-test:$version" }
  98. dependencies { // Kotlin Core features implementation "org.koin:koin-core:$version" // Android

    basic features implementation "org.koin:koin-android:$version" // Android basic + scope features implementation "org.koin:koin-android-scope:$version" // Android basic + scope + viewmodel features implementation "org.koin:koin-android-viewmodel:$version" // Kotlin Checks + JUnit & Mock features testImplementation "org.koin:koin-test:$version" }
  99. Before starting with Koin…

  100. Try the getting started projects * look at code samples

  101. None
  102. Read the quick references

  103. Add it step by step to your project * start

    with small components
  104. Check your modules

  105. Review your software design

  106. Keep it simple

  107. Documentation reference for deeper look * or even in source

    code
  108. None
  109. and …

  110. StackOverflow, Github & Slack for any question

  111. None
  112. @insertkoin_io koin-developers

  113. Thanks! Early adopters, Community feedback, Conference, talks, meetups…

  114. Spread the word!

  115. insert-koin.io

  116. Arnaud Giuliani - @arnogiu Thank you!