依赖注入框架--koin

  • koin 一个看似官方的注入依赖框架.

  • 资料来源:

    https://insert-koin.io/
    https://www.jianshu.com/p/bccb93a78cee

  • 更新

    1
    2
    20.03.11 初始化
    20.06.05 重新编辑内容,补充新内容

导语

依赖注入…可以把类想象成一个实体,依赖注入是个注射器,可向类中注射增加属性和对象.

这个过程类本身是无感的,所以可以把一些类之间的依赖关系改成依赖注射(入),降低类之间的耦合.

Android 中依赖注入首推自然是 G 家的 Dagger2 了.但是学习曲线那是相当陡峭(说起来也奇怪两年前对android 的学习也止步于 Dagger).

Dagger2 为了 java 设计,实现对 jetpack 的 ViewModel 支持很费劲,当然 google 在努力解决这一点.

所以呢 Google 继续努力,我转 koin 了.

koin

第一眼感觉是 Kotlin 官方库..

官方的介绍是: 无代理,无代码生成,无反射,纯 kotlin 编写的轻量级注入框架.

这一篇是结合 Nowakelock 说一下 koin 的基本使用

严重警告: 源码中大量使用了 DSL 语法和内联函数,如果不熟悉上述两种写法,谨慎翻源码.原理介绍的文章: 当Koin撞上ViewModel

目的

NoWakeLock 使用官方的 AAC 框架,用到依赖注入的主要有

  • Room 注入 DataRepository.需要使用全局单例.

  • DataRepository 注入 ViewModel,这里是个依赖注入的嵌套,没读完文档真是个坑.

  • ViewModel 注入 Fragment.

  • ViewModel 注入 Activity.

  • koin 的 ViewModel 其实还有坑,后面再说.

使用请参考 官方文档.或者这一篇 Koin–适用于Kotlin的超好用依赖注入框架,Dagger替代者,Koin史上最详细解说,一篇就够了,妈妈再也不用担心我不会依赖注入了

安装

koin 最新的版本是 koin_version= "2.1.5"

  • 添加依赖

    1
    2
    3
    4
    5
    6
    7
    8
    // Koin for Android
    implementation "org.koin:koin-android:$koin_version"

    // Koin Android Scope feature
    implementation "org.koin:koin-android-scope:$koin_version"

    // Koin Android ViewModel feature
    implementation "org.koin:koin-android-viewmodel:$koin_version"
  • 如果是 AndroidX

    1
    2
    3
    4
    5
    6
    7
    8
    // Koin AndroidX Scope feature
    implementation "org.koin:koin-androidx-scope:$koin_version"

    // Koin AndroidX ViewModel feature
    implementation "org.koin:koin-androidx-viewmodel:$koin_version"

    // Koin AndroidX Fragment Factory (unstable version)
    implementation "org.koin:koin-androidx-fragment:$koin_version"

基础使用

kotlin 注入声明是在 koin 的 module 中.

1
2
3
val myModule = module {
// Your definitions ...
}

koin 的注入是惰性的,仅仅在请求对应组件时才进行解析.而且仅仅解析需要的组件.

注入方式有 3 种

  • Factory: 如同声明一个普通对象.
  • Single: 单例模式.无论在类中声明几次都指向同一个对象.
  • viewModel: 这个特殊一点,声明的是我们继承 lifecycle 的 ViewModel.

在类中实例化的方式也有 3 种,基本上与声明一个新对象相同.

  • val person: Person by inject()
  • val person2 by inject<Person>()
  • val person3 = get<Person>()

对于带参数的类声明,可以通过 get() 获取到已经声明的类.如同 DataRepository 需要持有 AppDatabase 一样.

startKoin{} 中声明 androidContext([email protected]) ,后面可以直接引用方便获取 Context.

初始化.koin 需要在 Application 中初始化要注入的对象.

  • 在 Application.onCreate() 中声明一个 startKoin{} .
  • startKoin{} 中就是要注入的对象.

简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var noWakeLockModule = module {
single { AppDatabase.getInstance(androidApplication() }
single { DataRepository(get()) }
}

class BasicApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin{
androidContext(this@BasicApp)
listOf(noWakeLockModule)
}
}
}

NoWakelock 使用

DataRepository 主要是声明一个 DataRepository 的接口,然后传入一个实现接口的实例,作为一个隔离,这样在 ViewModel 中只要传入不同的实现就可以实现不同模块相似的功能.

ViewModel 则是配合 DataRepository 的接口,做到尽量复用.

下面两个例子,实现的效果是相同的.

FRepository

有一个 FRepository 接口,对应获取数据库的信息,在 wakelcok/alarm/service 有对应的 IAlarmR/IServiceR/IWakelockR 的实现.FViewModel 中会引用 FRepository 接口,通过传入不同的FRepository 实现多态.

1
2
3
4
5
6
7
8
9
10
11
12
interface FRepository {...}

class IAlarmR(private val alarmDao: AlarmDao) : FRepository {...}

class IServiceR(private val serviceDao: ServiceDao) : FRepository {...}

class IWakelockR(private val wakeLockDao: WakeLockDao) : FRepository {...}

class FViewModel(
packageName: String = "",
private var FRepository: FRepository
) : ViewModel() {...}

对于同一类型,koin 可以声明 named("AR") 设置别名以区分.

FViewModel 还需要一个 packageName 的参数,声明时可以通过类似 Lambda 表达式的参数引用形式,传入参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//单例
single<FRepository>(named("AR")) {
IAlarmR(
AppDatabase.getInstance(BasicApp.context).alarmDao()
)
}

...

//viewmodel
viewModel(named("VMA")) { (packageName: String) ->
FViewModel(
packageName,
get(named("AR"))
)
}

对应的 Fragment 中 FViewModel 的声明,传入参数通过 parametersOf(packageName) 传入,多个参数亦是如此.

1
2
3
4
5
//基类中默认是 wakelcok 的 FRepository 实现,默认是懒加载的形式.
open val viewModel: FViewModel by inject(named("VMA")) { parametersOf(packageName) }

//子类如果需要,直接重写 viewModel,这样做到了 viewmodel 的复用.
override val viewModel: FViewModel by inject(named("VMA")) { parametersOf(packageName) }

InfoRepository

还有一个例子不是子类继承父类,然后覆盖父类实现多态.

类似的有 InfoRepository 的接口,对应有 wakelcok/alarm/service 的实现.InfoViewModel 会引用 InfoRepository 的不同实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
interface InfoRepository {...}

class IAlarmIR(private val infoDao: InfoDao) : InfoRepository{...}

class IServiceIR(private val infoDao: InfoDao) : InfoRepository {....}

class IWakelockIR(private val infoDao: InfoDao) : InfoRepository {...}

class InfoViewModel(
val name: String,
val packageName: String,
private var infoRepository: InfoRepository
) : ViewModel() {...}

与前一个例子不同的是这里不是通过子类继承覆盖父类的形式实现,多态,这里是通过传入一个 type 类型通过 when 来判断该实例化的 InfoRepository.

模块声明用到多个参数时,与单个参数基本形式相同.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
single<InfoRepository>(named("IRA")) {
IAlarmIR(AppDatabase.getInstance(BasicApp.context).infoDao())
}

...

viewModel(named("VMI")) { (name: String, packageName: String, type: String) ->
InfoViewModel(
name,
packageName,
when (type) {
"alarm" -> {
get(named("IRA"))
}
"service" -> {
get(named("IRS"))
}
"wakelock" -> {
get(named("IRW"))
}
else -> {
get(named("IRW"))
}
}
)
}

InfoFragment 中实例化

1
2
//直接传入多个参数,在 parametersOf.
val viewModel: InfoViewModel by inject(named("VMI")) { parametersOf(name, packageName, type) }

Nowakelock 采用的单 Activity 多 Fragment 的实现,肯定少不了 Fragement 之间互相共享数据.

一个推荐的形式是 MainActivity 持有一个 MainViewModel 然后在 Fragment 中获取 MainViewModel ,不同Fragment 获取的 MainViewModel 是一个实例,这样就实现了数据共享而且对 MainActivity 无感.

很美好是不是?但 koin 无法实现,即 koin 目前还没办法实现获取到相同的 ViewModel 实例,哪怕这些 ViewModel 是继承自 jetpack 包的 ViewModel.

如果有类似需求,只能采用 jetpack 官方的实现,具体参考 ViewModel 概览

尾巴

  • 依赖注入的使用真的是解耦了太多了,配合 jetpack 和 kotlin 真的很好用.