Dependency Injection for Kotlin Apps with Koin

Dependency Injection for Kotlin Apps with Koin

E02cd1d0fc5c51ac491b484a258a63a6?s=128

Arnaud GIULIANI

November 30, 2019
Tweet

Transcript

  1. Dependency injection for Kotlin apps with Koin Arnaud Giuliani @arnogiu

  2. ! Freelance Software Eng " Mobile First Lead of Koin

    project Arnaud GIULIANI
  3. Why a new dependency injection framework?

  4. The CoffeeMaker App ☕ * aka the thermosiphon dagger’s nightmare

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

  6. The Heater

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

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

    override fun on() { println("~ ~ ~ heating ~ ~ ~") heating = true } override fun off() { heating = false } override fun isHot(): Boolean = heating }
  9. The Thermosiphon

  10. interface Pump { fun pump() }

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

    if (heater.isHot()){ println("=> => pumping => =>") } } }
  12. The CoffeeMaker

  13. class CoffeeMaker(val pump: Pump, val heater: Heater) { fun brew()

    { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } }
  14. The CoffeeApp

  15. class CoffeeApp { val coffeeMaker: CoffeeMaker fun run() { coffeeMaker.brew()

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

    how to assemble it?
  17. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater) // bootstrap from an injector // ---- // heater : Heater = ElectricHeater() // pump : Pump = Thermosiphon(heater) // coffeeMaker = CoffeeMaker(heater,pump) val coffeeMaker = Injector.get<CoffeeMaker>()
  18. { Thermosiphon(get()) } { ElectricHeater() } Provide Provide

  19. Koin

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

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

    implementation ‘org.koin:koin-core:$version’ } Gradle Setup
  22. // declare definitions here module { }

  23. module { // definition single | factory { instance creation

    expression } }
  24. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater)
  25. 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
  26. 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 coffeeParts = module { single<Pump>{ Thermosiphon(get()) } single<Heater> { ElectricHeater() } } * also multi modules
  27. Koin Component a class that can use the Koin container

  28. ✅ 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…)
  29. class CoffeeApp : KoinComponent { val coffeeMaker: CoffeeMaker by inject()

    fun run() { coffeeMaker.brew() } }
  30. The CoffeeApp

  31. startKoin to start the Koin container

  32. class CoffeeApp : KoinComponent { val coffeeMaker: CoffeeMaker by inject()

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

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

    { startKoin { modules(coffeeMakerModule) } }
  35. fun run() { coffeeMaker.brew() } } fun main(vararg args: String)

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

    => => [_]P coffee! [_]P
  37. None
  38. Can’t create Android components directly

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

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

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

    // Add Android Koin Logger startKoin { androidLogger() modules(coffeeMakerModule) } } }
  42. class MyApplication : Application() { override fun onCreate() { super.onCreate()

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

    }
  44. Get the Android context?

  45. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    // Reference Android context in Koin startKoin { androidLogger() androidContext(this@MyApplication) modules(coffeeMakerModule) } } }
  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 coffeeMaker : CoffeeMaker) val myModule = module {

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

    // will create MyPresenter on each call factory { MyPresenter(get()) } } 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’ implementation ‘org.koin:koin-androidx-viewmodel:$version’ } Gradle Setup
  53. ✅ Koin extensions for Android ViewModel

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

    module { viewModel { MyViewModel(get()) } }
  55. //... viewModel { MyViewModel(get()) } } class MyActivity : AppCompatActivity()

    { // inject ViewModel val myViewModel: MyViewModel by viewModel()
  56. //... viewModel { MyViewModel(get()) } } class MyFragment : Fragment()

    { // inject ViewModel val myViewModel: MyViewModel by viewModel()
  57. //... viewModel { MyViewModel(get()) } } class MyFragment : Fragment()

    { // inject ViewModel from parent activity val myViewModel: MyViewModel by sharedViewModel()
  58. Injecting Bundle State in ViewModel

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

    Fragments feature implementation ‘org.koin:koin-androidx-viewmodel:$version’ } Gradle Setup 2.1.0-alpha-4
  60. // Inject your state class StateViewModel(val state: SavedStateHandle) : ViewModel()

    // use injection parameter to get state viewModel { (handle: SavedStateHandle, id: String) -> StateViewModel(handle, id, get()) } // Provide initial state as parameter class MVVMActivity : AppCompatActivity() { val mySavedVM: StateViewModel by viewModel { parametersOf(Bundle(), "vm1") } }
  61. // Inject your state class StateViewModel( val handle: SavedStateHandle, val

    id: String, val service: SimpleService) : ViewModel() // use injection parameter to get state viewModel { (handle: SavedStateHandle) -> StateViewModel(handle, id, get()) } // Provide initial state as parameter class MVVMActivity : AppCompatActivity() { val mySavedVM: StateViewModel by viewModel { parametersOf(Bundle(), "vm1") } }
  62. // Inject your state class StateViewModel( val handle: SavedStateHandle, val

    id: String, val service: SimpleService) : ViewModel() // use injection parameter to get state viewModel { (handle: SavedStateHandle, id: String) -> StateViewModel(handle, id, get()) } // Provide initial state as parameter class MVVMActivity : AppCompatActivity() { val mySavedVM: StateViewModel by viewModel { parametersOf(Bundle()) } }
  63. Inject your Fragments

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

    Fragments feature implementation ‘org.koin:koin-androidx-fragment:$version’ } Gradle Setup 2.1.0-alpha-4
  65. // Use constructor injection :) class MyFragment(val session: Session) :

    Fragment() // Declare your fragment val module = module { single { Session() } fragment { MyFragment(get()) } } // Setup in Activity class MyMActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // setup factory setupKoinFragmentFactory() super.onCreate(savedInstanceState)
  66. // Use constructor injection :) class MyFragment(val session: Session) :

    Fragment() // Declare your fragment val module = module { single { Session() } fragment { MyFragment(get()) } } // Setup in Activity class MyMActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // setup factory setupKoinFragmentFactory() super.onCreate(savedInstanceState)
  67. // Setup in Activity class MyMActivity : AppCompatActivity() { override

    fun onCreate(savedInstanceState: Bundle?) { // setup factory setupKoinFragmentFactory() super.onCreate(savedInstanceState) // Call your Fragment supportFragmentManager.beginTransaction() .replace(R.id.fragment, MyFragment::class.java, null, null) .commit() } }
  68. // Setup in Activity class MyMActivity : AppCompatActivity() { override

    fun onCreate(savedInstanceState: Bundle?) { // setup factory setupKoinFragmentFactory() super.onCreate(savedInstanceState) // Call your Fragment supportFragmentManager.beginTransaction() .replace(R.id.fragment, MyFragment::class.java, null, null) .commit() } }
  69. Ktor Web Applications

  70. repositories { jcenter() } dependencies { // Koin for Ktor

    compile ‘org.koin:koin-ktor:$version’ // Koin SLF4J Logger compile ‘org.koin:koin-logger-slf4j:$version’ } Gradle Setup
  71. fun main(args: Array<String>) { // Start Ktor embeddedServer(Netty, commandLineEnvironment(args)).start() }

    fun Application.main() { install(DefaultHeaders) install(CallLogging) // Routing section routing { get("/hello") { call.respondText(...) } } }
  72. fun main(args: Array<String>) { // Start Ktor embeddedServer(Netty, commandLineEnvironment(args)).start() }

    fun Application.main() { install(DefaultHeaders) install(CallLogging) // Routing section routing { get("/coffee") { call.respondText(...) } } }
  73. fun Application.main() { install(DefaultHeaders) install(CallLogging) install(Koin) { slf4jLogger() modules(coffeeMakerModule) }

    // Routing section routing { get(“/coffee") { call.respondText(...) } } }
  74. fun Application.main() { install(DefaultHeaders) install(CallLogging) install(Koin) { slf4jLogger() modules(coffeeMakerModule) }

    val coffeeMaker by inject() // Routing section routing { get("/coffee") { call.respondText(...) } } }
  75. fun Application.main() { install(DefaultHeaders) install(CallLogging) install(Koin) { slf4jLogger() modules(coffeeMakerModule) }

    val coffeeMaker by inject() // Routing section routing { get("/coffee") { call.respondText(coffeeMaker.printBrew()) } } }
  76. Application, Route, Routing … are extended as KoinComponent! out of

    the box! ✅ Koin extensions for Ktor
  77. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater) val coffeeMakerModule = module(createdAtStart = true) { single<CoffeeMaker>() singleBy<Pump,Thermosiphon>() singleBy<Heater,ElectricHeater>() }
  78. Testing easily with Koin ✅

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

    testImplementation ‘org.koin:koin-test:$version’ } Gradle Setup In rewrite!
  80. Checking your modules ✔

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

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

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

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } } }
  84. class CoffeeMakerTest : KoinTest { val coffeeMaker: CoffeeMaker by inject()

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } declareMock<Heater>() } }
  85. 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() } }
  86. More about Koin✨

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

    context - Scope API - Android, Scopes & lifecycle - Injection Parameters - AndroidX - Koin for Java - Koin for Ktor
  88. Koin 2.0

  89. Since first 1.0 version…

  90. Simple DSL module, single, factory, get

  91. Pragmatic API ✌ KoinComponent, get, inject

  92. None
  93. None
  94. * startup & Kotlin overhead ** split modules startup if

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

  96. http://bit.ly/ready4koin

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

    context - Scope API - Android, Scopes & lifecycle - Injection Parameters - AndroidX - Koin for Java - Koin for Ktor
  98. - Container DSL - Qualifiers (string/type) - Global vs isolated

    context - Scope API - Android, Scopes & lifecycle - Injection Parameters - AndroidX - Koin for Java - Koin for Ktor
  99. Dependency injection & Service Locator?

  100. Dependency injection & Service Locator

  101. class Thermosiphon(val heater : Heater) : Pump class ElectricHeater() :

    Heater module { single<Pump>{ Thermosiphon(get()) } single<Heater> { ElectricHeater() } } Dependency Injection - Constructor injection configured via Koin DSL - Your classes are not dependant to Koin API - Instances are called by Koin Container - Functional DSL Configuration: parameters, context, scopes…
  102. class CoffeeApp : KoinComponent { val coffeeMaker by inject<CoffeeMaker>() }

    Service Locator - Use the Koin API to retrieve a dependency - “Host” class not created by Koin
  103. Is service locator bad?

  104. class CoffeeApp : KoinComponent { // Lazily ask for instance

    val coffeeMaker : CoffeeMaker by inject() } Service Locator class CoffeeApp { lateinit var coffeeMaker : CoffeeMaker init { // ask injection of setters DI.inject(this) } } Setter Injection
  105. 0

  106. 80% of your Koin usage will be configuration 20% will

    be platform extensions Limit your dependency to Koin as much as possible
  107. The choice between Service Locator and Dependency Injection is less

    important than the principle of separating service configuration from the use of services within an application. Martin Fowler
  108. Functional Dependency Injection

  109. Function and Container Context

  110. val coffeeMakerModule = module { single { CoffeeMaker(get(),get()) } single<Pump>{

    Thermosiphon(get()) } single<Heater> { ElectricHeater() } } typealias Definition<T> = Scope.(DefinitionParameters) -> T
  111. Definition Parameters DSL Extension Scope

  112. Function to inject parameters

  113. class MyPresenter(val id : String) val myModule = module {

    factory { (id : String) -> MyPresenter(id)} }
  114. val myModule = module { factory { (id : String)

    -> MyPresenter(id)} } class MyActivity : AppCompatActivity(){ val presenter : MyPresenter by inject { parametersOf("42")} }
  115. class MyViewModel(val id : String) val myModule = module {

    viewModel { (id : String) -> MyViewModel(id)} }
  116. val myModule = module { viewModel { (id : String)

    -> MyViewModel(id)} } class MyActivity : AppCompatActivity(){ val presenter : MyViewModel by viewModel { parametersOf("42")} }
  117. ✨ Simpler declaration with Reflection ✨

  118. A.k.a “Get rid of get() get() get() get() get()”

  119. Functional Dependency Injection 0

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

    implementation ‘org.koin:koin-core-ext:$version’ } Gradle Setup
  121. 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() } }
  122. class ElectricHeater : Heater class Thermosiphon(val heater: Heater) : Pump

    class CoffeeMaker(val pump: Pump, val heater: Heater) val coffeeMakerModule = module { single<CoffeeMaker>() singleBy<Pump,Thermosiphon>() singleBy<Heater,ElectricHeater>() }
  123. Available out of the box for pure JVM platforms (Ktor,

    TornadoFx…)
  124. repositories { jcenter() } dependencies { // Koin for Android

    implementation ‘org.koin:koin-android-ext:$version’ implementation ‘org.koin:koin-androidx-ext:$version’ } Gradle Setup
  125. class MyViewModel(val coffeeMaker : CoffeeMaker) : ViewModel() val coffeeMakerModule =

    module { viewModel<MyViewModel>() }
  126. Add following pro guard rules https://insert-koin.io/docs/2.0/quick-references/experimental-features/#proguard-rule

  127. Check performances Pure functional DSL declaration is 4x - 10x

    faster * definition execution time ** depending on devices (< Android 6)
  128. Let’s migrate to Koin, what else?

  129. Refactoring is not always sustainable

  130. « Hard to switch from our current DI » Top

    #1 issue
  131. None
  132. What if you could… run it easily in a module

  133. startKoin { modules(…) } loadKoinModules(…) Definition<Heater> Definition<Pump> Definition<CoffeeMaker> unloadKoinModules(…)

  134. Making dynamic modules with Koin - Load dynamic modules/definitions -

    Change implementation on the fly - Easy to compose step by step
  135. Onboarding Wallet Profile Transactions Profile v2 Payments Card Management Onboarding

    Onboarding v2
  136. Dynamic Modules Architecture - Registry / Plugin architecture - Feature

    <> Libraries isolation (modules) - Navigation, Feature Flagging, A/B testing …
  137. What’s next?

  138. None
  139. Tips & Best pratices

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

  141. start.insert-koin.io

  142. doc.insert-koin.io

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

    with small components
  144. Check your modules ✅

  145. Keep it simple

  146. and …

  147. StackOverflow, Github & Slack for any question

  148. None
  149. @insertkoin_io koin-developers

  150. Thank you! All the community…

  151. stable_version = “2.0.1" unstable_version = “2.1.0-alpha-4” repositories { jcenter() }

    dependencies { // Core features compile "org.koin:koin-core:$version" // Ktor compile "org.koin:koin-ktor:$version" compile "org.koin:koin-logger-slf4j:$version" // Android implementation "org.koin:koin-android:$version"
  152. dependencies { // Core features compile "org.koin:koin-core:$version" // Ktor compile

    "org.koin:koin-ktor:$version" compile "org.koin:koin-logger-slf4j:$version" // Android implementation "org.koin:koin-android:$version" // Android + ViewModel implementation "org.koin:koin-android-viewmodel:$version" // JUnit & Mockito testImplementation "org.koin:koin-test:$version" }
  153. dependencies { // Core features compile "org.koin:koin-core:$version" // Ktor compile

    "org.koin:koin-ktor:$version" compile "org.koin:koin-logger-slf4j:$version" // Android implementation "org.koin:koin-android:$version" // Android + ViewModel implementation "org.koin:koin-android-viewmodel:$version" // JUnit & Mockito testImplementation "org.koin:koin-test:$version" }
  154. dependencies { // Core features compile "org.koin:koin-core:$version" // Ktor compile

    "org.koin:koin-ktor:$version" compile "org.koin:koin-logger-slf4j:$version" // Android implementation “org.koin:koin-android:$version” // Android + ViewModel implementation "org.koin:koin-android-viewmodel:$version" // JUnit & Mockito testImplementation "org.koin:koin-test:$version" }
  155. dependencies { // Core features compile "org.koin:koin-core:$version" // Ktor compile

    "org.koin:koin-ktor:$version" compile "org.koin:koin-logger-slf4j:$version" // Android implementation "org.koin:koin-android:$version" // Android + ViewModel implementation "org.koin:koin-android-viewmodel:$version" // JUnit & Mockito testImplementation "org.koin:koin-test:$version" }
  156. insert-koin.io

  157. Arnaud Giuliani - @arnogiu Thank you!