Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dependency Injection for Kotlin Apps with Koin

Dependency Injection for Kotlin Apps with Koin

Arnaud GIULIANI

November 30, 2019
Tweet

More Decks by Arnaud GIULIANI

Other Decks in Programming

Transcript

  1. class ElectricHeater : Heater { var heating: Boolean = false

    override fun on() { println("~ ~ ~ heating ~ ~ ~") heating = true } override fun off() { heating = false } override fun isHot(): Boolean = heating }
  2. class Thermosiphon(val heater: Heater) : Pump{ override fun pump() {

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

    { heater.on() pump.pump() println(" [_]P coffee! [_]P ") heater.off() } }
  4. 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>()
  5. - Intuitive DSL to describe it - Lightweight container to

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

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

    class CoffeeMaker(val pump: Pump, val heater: Heater)
  8. 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
  9. 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
  10. ✅ 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…)
  11. class CoffeeApp : KoinComponent { val coffeeMaker: CoffeeMaker by inject()

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

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

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

    => => [_]P coffee! [_]P
  15. repositories { jcenter() } dependencies { // Koin for Android

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

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

    // Reference Android context in Koin startKoin { androidLogger() androidContext(this@MyApplication) modules(coffeeMakerModule) } } }
  18. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    // Reference Android context in Koin startKoin { androidLogger() androidContext(this@MyApplication) modules(coffeeMakerModule) } } }
  19. class MyComponent(val context : Context) val myModule = module {

    single { MyComponent(androidContext()) } }
  20. class MyPresenter(val coffeeMaker : CoffeeMaker) val myModule = module {

    // will create MyPresenter on each call factory { MyPresenter(get()) } }
  21. 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() }
  22. repositories { jcenter() } dependencies { // Koin for Android

    + ViewModel features implementation ‘org.koin:koin-android-viewmodel:$version’ implementation ‘org.koin:koin-androidx-viewmodel:$version’ } Gradle Setup
  23. //... viewModel { MyViewModel(get()) } } class MyActivity : AppCompatActivity()

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

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

    { // inject ViewModel from parent activity val myViewModel: MyViewModel by sharedViewModel()
  26. repositories { jcenter() } dependencies { // Koin for Android

    Fragments feature implementation ‘org.koin:koin-androidx-viewmodel:$version’ } Gradle Setup 2.1.0-alpha-4
  27. // 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") } }
  28. // 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") } }
  29. // 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()) } }
  30. repositories { jcenter() } dependencies { // Koin for Android

    Fragments feature implementation ‘org.koin:koin-androidx-fragment:$version’ } Gradle Setup 2.1.0-alpha-4
  31. // 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)
  32. // 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)
  33. // 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() } }
  34. // 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() } }
  35. repositories { jcenter() } dependencies { // Koin for Ktor

    compile ‘org.koin:koin-ktor:$version’ // Koin SLF4J Logger compile ‘org.koin:koin-logger-slf4j:$version’ } Gradle Setup
  36. 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(...) } } }
  37. 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(...) } } }
  38. fun Application.main() { install(DefaultHeaders) install(CallLogging) install(Koin) { slf4jLogger() modules(coffeeMakerModule) }

    val coffeeMaker by inject() // Routing section routing { get("/coffee") { call.respondText(...) } } }
  39. 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()) } } }
  40. 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>() }
  41. repositories { jcenter() } dependencies { // Koin for Tests

    testImplementation ‘org.koin:koin-test:$version’ } Gradle Setup In rewrite!
  42. class ModuleTest { @Test fun `check all definitions from coffeeMakerModule`()

    { startKoin { modules(coffeeMakerModule) }.checkModules() } }
  43. class CoffeeMakerTest : KoinTest { val coffeeMaker: CoffeeMaker by inject()

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

    val heater: Heater by inject() @Test fun testHeaterIsTurnedOnAndThenOff() { startKoin { modules(coffeeMakerModule) } declareMock<Heater>() } }
  45. 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() } }
  46. - Container DSL - Qualifiers (string/type) - Global vs isolated

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

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

    context - Scope API - Android, Scopes & lifecycle - Injection Parameters - AndroidX - Koin for Java - Koin for Ktor
  49. 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…
  50. class CoffeeApp : KoinComponent { val coffeeMaker by inject<CoffeeMaker>() }

    Service Locator - Use the Koin API to retrieve a dependency - “Host” class not created by Koin
  51. 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
  52. 0

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

    be platform extensions Limit your dependency to Koin as much as possible
  54. 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
  55. val coffeeMakerModule = module { single { CoffeeMaker(get(),get()) } single<Pump>{

    Thermosiphon(get()) } single<Heater> { ElectricHeater() } } typealias Definition<T> = Scope.(DefinitionParameters) -> T
  56. class MyPresenter(val id : String) val myModule = module {

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

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

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

    -> MyViewModel(id)} } class MyActivity : AppCompatActivity(){ val presenter : MyViewModel by viewModel { parametersOf("42")} }
  60. repositories { jcenter() } dependencies { // Koin for Android

    implementation ‘org.koin:koin-core-ext:$version’ } Gradle Setup
  61. 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() } }
  62. 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>() }
  63. repositories { jcenter() } dependencies { // Koin for Android

    implementation ‘org.koin:koin-android-ext:$version’ implementation ‘org.koin:koin-androidx-ext:$version’ } Gradle Setup
  64. Check performances Pure functional DSL declaration is 4x - 10x

    faster * definition execution time ** depending on devices (< Android 6)
  65. Making dynamic modules with Koin - Load dynamic modules/definitions -

    Change implementation on the fly - Easy to compose step by step
  66. Dynamic Modules Architecture - Registry / Plugin architecture - Feature

    <> Libraries isolation (modules) - Navigation, Feature Flagging, A/B testing …
  67. 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"
  68. 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" }
  69. 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" }
  70. 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" }
  71. 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" }