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

Getting Big Payouts with Koin -- droidcon Lisbon

Rick Busarow
September 09, 2019

Getting Big Payouts with Koin -- droidcon Lisbon

Thanks to its clean design, Koin is quickly gaining popularity as a service locator framework. Highlights include its excellent documentation, accessible api, and the pure Kotlin source code.

In this talk, we’ll go over basic usage like factories, singles, and modules. We’ll talk about scoping and lazy-loading modules, and we’ll look at its integration with Android screens and ViewModels. Then, and most importantly, we’ll see how we can use Koin to quickly and easily generate mocked dependencies for clean and hermetic tests.

Rick Busarow

September 09, 2019
Tweet

More Decks by Rick Busarow

Other Decks in Programming

Transcript

  1. @rbusarow class UserRepository { internal val service: UserService = retrofit.create<UserService>()

    internal val dao: UserDao = database.userDao } Normal Control Flow
  2. @rbusarow class UserRepository { internal val okHttpClient: OkHttpClient = OkHttpClient()

    internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val database: Database = Database.init(APP_CONTEXT) internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Normal Control Flow
  3. @rbusarow class UserRepository { internal val okHttpClient: OkHttpClient = OkHttpClient()

    internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val database: Database = Database.init(APP_CONTEXT) internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Normal Control Flow
  4. @rbusarow Service Location Dependency Injection class UserViewModel { private val

    repository = Locator.createRepository() } Inversion of Control
  5. @rbusarow Service Location Dependency Injection class UserViewModel { private val

    repository = Locator.createRepository() } class UserViewModel(val repository: UserRepository) { lateinit var otherDependency : OtherDependency } Inversion of Control
  6. class UserScreen : Fragment() { lateinit var viewModel: UserViewModel override

    fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } @rbusarow Field Injection
  7. class UserScreen : Fragment() { lateinit var viewModel: UserViewModel override

    fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } @rbusarow Field Injection
  8. @rbusarow class UserScreen : Fragment() { lateinit var viewModel: UserViewModel

    override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } object Injector { fun inject(screen: UserScreen) { screen.viewModel = TODO() } } Field Injection public mutable late init
  9. @rbusarow class UserViewModel { val client = OkHttpClient() val retrofit

    = Retrofit.Builder() .baseUrl("https://example.dev") .client(client) .build() val service = retrofit.create<UserService>() val database = Database.init(APP_CONTEXT) val dao = database.userDao val repository = UserRepository(service = service, dao = dao) } Constructor Injection
  10. @rbusarow class UserViewModel { val client = OkHttpClient() val retrofit

    = Retrofit.Builder() .baseUrl("https://example.dev") .client(client) .build() val service = retrofit.create<UserService>() val database = Database.init(APP_CONTEXT) val dao = database.userDao val repository = UserRepository(service = service, dao = dao) } Constructor Injection
  11. @rbusarow class UserViewModel(service: UserService, dao: UserDao) { val repository =

    UserRepository(service = service, dao = dao) } Constructor Injection
  12. @rbusarow class UserScreen : Fragment() { lateinit var viewModel :

    UserViewModel override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Field Injection…
  13. @rbusarow class UserScreen : Fragment() { lateinit var viewModel :

    UserViewModel override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Field Injection…
  14. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { ViewModelProviders .of(this) .get(UserViewModel::class.java) } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location
  15. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location
  16. @rbusarow class UserScreen : Fragment() { private val viewModel by

    lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location object Locator { inline fun <reified T : ViewModel> get(): T = … }
  17. @rbusarow class UserScreen : Fragment() { private val presenter by

    lazy { Locator.get<UserPresenter>() } private val viewModel by lazy { Locator.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } Service Location object Locator { inline fun <reified T : ViewModel> get(): T = … }
  18. @rbusarow class UserScreen : Fragment() { private val presenter by

    lazy { Locator.instance.get<UserPresenter>() } private val viewModel by lazy { Locator.instance.get<UserViewModel>() } override fun onAttach(context: Context) { Injector.inject(this) super.onAttach(context) } } interface Locator { fun <T : ViewModel> get(): T = … companion object { lateinit var instance: Locator } } Service Location
  19. class Koin { val graph: Graph fun <T> inject(): Lazy<T>

    fun <T> get(): T fun <S, P> bind(s: S): S fun createScope(scopeId: ScopeID, qualifier: Qualifier): Scope } class KoinApplication { val koin = Koin() fun modules(modules: List<Module>): KoinApplication fun logger(logger: Logger): KoinApplication fun unloadModules(modules: List<Module>) fun close() } @rbusarow Koin & KoinApplication
  20. class Koin { val graph: Graph fun <T> inject(): Lazy<T>

    fun <T> get(): T fun <S, P> bind(s: S): S fun createScope(scopeId: ScopeID, qualifier: Qualifier): Scope } class KoinApplication { val koin = Koin() fun modules(modules: List<Module>): KoinApplication fun logger(logger: Logger): KoinApplication fun unloadModules(modules: List<Module>) fun close() } @rbusarow Koin & KoinApplication
  21. class Koin { val graph: Graph fun <T> inject(): Lazy<T>

    fun <T> get(): T fun <S, P> bind(s: S): S fun createScope(scopeId: ScopeID, qualifier: Qualifier): Scope } class KoinApplication { val koin = Koin() fun modules(modules: List<Module>): KoinApplication fun logger(logger: Logger): KoinApplication fun unloadModules(modules: List<Module>) fun close() } @rbusarow Koin & KoinApplication Not an Android Application
  22. @rbusarow object GlobalContext { internal var app: KoinApplication? = null

    fun get(): KoinApplication fun getOrNull(): KoinApplication? fun start(koinApplication: KoinApplication) fun stop() = synchronized(this) { app?.close() app = null } } GlobalContext
  23. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication
  24. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication
  25. @rbusarow fun startKoin(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create()

    GlobalContext.start(koinApplication) appDeclaration(koinApplication) koinApplication.createEagerInstances() return koinApplication } fun koinApplication(appDeclaration: KoinAppDeclaration): KoinApplication { val koinApplication = KoinApplication.create() appDeclaration(koinApplication) return koinApplication } startKoin & koinApplication
  26. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() startKoin { // route all Koin logs through android.util.Log androidLogger() } } } MyApplication.kt
  27. @rbusarow private class TimberLogger(level: Level = Level.INFO) : Logger(level) {

    override fun log(level: Level, msg: MESSAGE) { if (this.level <= level) { when (this.level) { Level.DEBUG -> Timber.tag(KOIN_TAG).d(msg) Level.INFO -> Timber.tag(KOIN_TAG).i(msg) Level.ERROR -> Timber.tag(KOIN_TAG).e(msg) } } } } fun KoinApplication.timberLogger(level: Level = Level.INFO): KoinApplication { KoinApplication.logger = TimberLogger(level) return this } TimberLogger.kt
  28. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() } } } MyApplication.kt
  29. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) } } } MyApplication.kt
  30. @rbusarow class MyApplication : Application() { override fun onCreate() {

    super.onCreate() Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } MyApplication.kt
  31. @rbusarow val dataModule = module { factory { Timber.v("Hello everyone!")

    UserRepository().also { Timber.v("Goodbye!") } } } Modules
  32. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } @rbusarow Connecting the Graph
  33. class MyApplication : Application() { override fun onCreate() { super.onCreate()

    Timber.plant(DebugTree()) startKoin { // route all Koin logs through Timber timberLogger() // provide Application context to the graph androidContext(this@MyApplication) modules(koinDemoModules) } } } @rbusarow Connecting the Graph
  34. @rbusarow val koinDemoModules: List<Module> get() = emptyListOf() val dataModule =

    module { factory { UserRepository() } } Connecting the Graph
  35. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule) val dataModule =

    module { factory { UserRepository() } } Connecting the Graph
  36. @rbusarow class UserRepository { internal val database: Database = Database.init(APP_CONTEXT)

    internal val okHttpClient: OkHttpClient = OkHttpClient() internal val retrofit: Retrofit = Retrofit.Builder() .baseUrl("https://example.dev") .client(okHttpClient) .build() internal val service: UserService = retrofit.create<UserService>() internal val dao: UserDao = database.userDao } Connecting the Graph
  37. @rbusarow val dataModule = module { factory<UserService> { TODO() }

    factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } Connecting the Graph
  38. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  39. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<UserDao> { TODO() } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  40. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  41. val dataModule = module { factory { OkHttpClient() } factory

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  42. @rbusarow Connecting the Graph val dataModule = module { factory

    { OkHttpClient() } factory { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory { UserRepository(service = get(), dao = get()) } }
  43. @rbusarow Modules val dataModule = module { factory<OkHttpClient> { OkHttpClient()

    } factory<Retrofit> { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory<UserRepository> { UserRepository(service = get(), dao = get()) } }
  44. val dataModule = module { factory<OkHttpClient> { OkHttpClient() } factory<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } factory<UserService> { get<Retrofit>().create<UserService>() } factory<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } factory<UserRepository> { UserRepository(service = get(), dao = get()) } } @rbusarow Connecting the Graph
  45. val dataModule = module { single<OkHttpClient> { OkHttpClient() } single<Retrofit>

    { Retrofit.Builder() .baseUrl("https://example.dev") .client(get<OkHttpClient>()) .build() } single<UserService> { get<Retrofit>().create<UserService>() } single<Database> { Database.init(androidContext()) } factory<UserDao> { get<Database>().userDao } single<UserRepository> { UserRepository(service = get(), dao = get()) } } @rbusarow Modules
  46. @rbusarow class UserScreen : Fragment() { val repository = get<UserRepository>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  47. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val viewModelModule

    = module { viewModel<UserViewModel> { UserViewModel(repository = get()) } } Accessing the Graph
  48. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, viewModelModule) val viewModelModule

    = module { viewModel<ViewModel> { ViewModel(repository = get()) } } Accessing the Graph
  49. @rbusarow class UserScreen : Fragment() { val viewModel = get<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  50. @rbusarow class UserScreen : Fragment() { val viewModel by viewModel<UserViewModel>()

    } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  51. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserRepository) : ViewModel() Accessing the Graph
  52. @rbusarow class UserScreen : Fragment() { val repository by inject<UserViewModel>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel(private val repository: UserViewModel) : ViewModel() Parameters
  53. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() Parameters
  54. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel>() } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() val viewModelModule = module { viewModel<UserViewModel> { (userId: String) -> ViewModel(userId = userId, repository = get()) } } Parameters
  55. @rbusarow class UserScreen : Fragment() { val repository by inject<UserRepository>()

    val viewModel by viewModel<UserViewModel> { parametersOf(“50 Cent”) } } class UserViewModel( private val userId: String, private val repository: UserRepository ) : ViewModel() val viewModelModule = module { viewModel<UserViewModel> { (userId: String) -> UserViewModel(userId = userId, repository = get()) } } Parameters
  56. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scoped<Preferences> { Preferences() } } Scope
  57. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scoped<Preferences> { Preferences() } } object Scopes val Scopes.login: StringQualifier get() = named("login") Scope
  58. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = Scopes.login) { scoped<Preferences> { Preferences() } } } object Scopes val Scopes.login: StringQualifier get() = named("login") Scope
  59. @rbusarow val koinDemoModules: List<Module> get() = listOf(dataModule, scopedModule, viewModelModule) val

    scopedModule = module { scope(scopeName = Scopes.login) { scoped<Preferences> { Preferences() } } } class Anywhere : KoinComponent { val loginScope = getKoin().createScope(Scopes.login, named("login")) val preferences by loginScope.inject<Preferences>() } Scope
  60. @rbusarow class Screen : Fragment() { val presenter by currentScope.inject<Presenter>()

    val viewModel by viewModel<ViewModel>() } val scopedModule = module { scope(named<Screen>()) { scoped<Presenter> { Presenter() } } scope(scopeName = Scopes.login) { scoped<Preferences> { Preferences() } } } Parameters
  61. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature Dynamic Features
  62. @rbusarow val featureModules = listOf(scopedModule, viewModelModule) private val loadFeature by

    lazy { loadKoinModules(featureModules) } fun initModules() = loadFeature class SomeFeatureActivity : Activity() { init { initModules() } } Dynamic Features
  63. @rbusarow internal class UserRepositoryTest { val service = mockk<UserService>() val

    dao = mockk<UserDao>() lateinit var repo: UserRepository @BeforeEach fun beforeEach() { repo = UserRepository(service, dao) } } Testing
  64. @rbusarow internal class UserRepositoryTest : KoinTest { val service =

    mockk<UserService>() val dao = mockk<UserDao>() lateinit var repo: UserRepository @BeforeEach fun beforeEach() { repo = UserRepository(service, dao) } } Testing
  65. @rbusarow internal class UserRepositoryTest : KoinTest { … @BeforeEach fun

    beforeEach() { startKoin { modules(koinDemoModules) } repo = get<UserRepository>() } @AfterEach fun afterEach() { getKoin().close() } } Testing
  66. @rbusarow internal class UserRepositoryTest : KoinTest { … val mocks

    = module(override = true) { single<UserService> { service } single<UserDao> { dao } } @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) } repo = get<UserRepository>() } @AfterEach fun afterEach() { getKoin().close() } } Testing
  67. @rbusarow internal class UserRepositoryTest : KoinTest { … val mocks

    = module(override = true) { single<UserService> { service } single<UserDao> { dao } } @BeforeEach fun beforeEach() { startKoin { modules(koinDemoModules) modules(mocks) } repo = get<UserRepository>() } … } Testing
  68. @rbusarow internal class ModuleTest : KoinTest { @BeforeEach fun beforeEach()

    { startKoin { modules(koinDemoModules) } } @AfterEach fun afterEach() { getKoin().close() } @Test fun `check modules`() { getKoin().checkModules() } } Testing