Koin是一个为Kotlin设计的轻量级依赖注入框架(依赖检索容器)。
关键词:DSL
、Light
、无代码生成。
引入依赖
koin是为Kotlin语言设计的框架,因此在多数使用到Kotlin的地方都可以使用。同时,Koin还为Android、Android Compose、Ktor提供了专用版本。
本节仅介绍Koin在Android中的引入方式,其它使用场景请参考官方文档。
-
在项目根级gradle脚本中添加maven仓库
repositories { ... mavenCentral() }
-
在模块级别添加对koin的依赖
koin_android_version= "3.2.2" implementation "io.insert-koin:koin-android:$koin_android_version" // (可选) Java兼容包 implementation "io.insert-koin:koin-android-compat:$koin_android_version" // (可选) Jetpack WorkManager支持 implementation "io.insert-koin:koin-androidx-workmanager:$koin_android_version" // (可选) Jetpack Navigation支持 implementation "io.insert-koin:koin-androidx-navigation:$koin_android_version"
使用
提供依赖
使用startKoin
开始提供依赖,Android中通常会在Application的onCreate
生命周期中定义。
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
// 提供Application实例的依赖项
androidContext(this@MyApplication)
// koin以模块的形式组织依赖项,使用modlues系列函数装载模块
modules(appModule)
}
}
private val appModule = module {
// 提供具体的依赖项
}
}
koin以模块的形式组织依赖项,因此,我们的依赖项需要定义在module中,使用module
函数可以定义一个module实例,而我们的依赖项就定义在传入到module
函数的lambda中。
private val appModule = module {
factory {
UserListAdapter()
}
factory {
File("config.json")
}
}
上面的例子中,我们在appModule
中定义了两个factory依赖项,当我们需要注入对应的依赖项实例时,Koin就会自动执行lambda,创建新的实例执行注入。
注入依赖项
定义好依赖项以后,我们就可以使用get
函数来注入/获取依赖项实例:
在普通类中注入
如果要在普通类中注入依赖项,需要为这个类实现KoinComponent
接口,然后就可以在其中调用get
来获取依赖实例了。
class ConfigParser : KoinComponent {
private configFile: File = get()
// private val configFile = get<File>()
private fun readConfig() {
// 在函数中也能获取到依赖项
val configFile: File = get()
}
}
在Android组件中注入
koin-android为Android中的ComponentCallbacks
提供了支持注入依赖的扩展,因此在Activity
、Fragment
、Service
等Android组件中,可以直接注入依赖,而无需实现KoinComponent
接口。
class MainActivity : AppCompatActivity() {
private val adapter: UserListAdapter = get()
...
}
延迟注入
借助Kotlin属性委托的语法特性,我们可以很容易的实现延迟注入,同时Koin也提供了inject
函数来支持延迟注入。
class MainActivity : AppCompatActivity() {
private val adapter: UserListAdapter by lazy { get() }
// 使用inject函数
// private val adapter: UserListAdapter by inject()
// private val adapter by inject<UserListAdapter>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
// 使用到时才会去创建实例
list.adapter = adapter
}
}
提供依赖时注入
可以在定义依赖项时,还获取其它的依赖项来初始化。
private val appModule = module {
factory {
File("config.json")
}
factory {
// Configuration接受一个File对象,此处Koin会自动获取config.json
// 的文件实例设置进去。
Configuration(get())
}
}
限定符 Qualifier
与Dagger一样,Koin在默认情况下也是通过依赖项的类型来确定要注入哪个类型实例的,如果我们在提供依赖时,定义了同一类型的两个不同实例,Koin将无法确定注入哪一个实例。为此,Koin提供了限定符Qualifier
用来区分不同的对象。
Koin提供了多种方式来创建Qualifier,其中最常使用的是named
函数。在定义依赖项时,将Qualifier作为参数传递,即可对当前依赖项进行限制。
private val appModule = module {
factory(named("config-file")) { File("config.json") }
factory(named("data-file")) { File("data.txt") }
}
使用时,在获取依赖项的地方使用同样的Qualifier,即可获取指定的依赖项:
val configFile: File = get(named("config-file"))
val dataFile: File by inject(named("data-file"))
除了named
+字符串参数的方式创建Qualifier外,Koin还提供了许多其它的方式来创建Qualifier:比如使用枚举对象、Class对象作为参数……更详细的内容请查阅源码或者官方文档,此处不再赘述。
作用域 Scope
通过作用域我们可以限制依赖项的生命周期以及查找范围,实际上Koin定义的每个依赖项都有其作用域,当定义依赖项未指定scope时,会使用rootScope
作为作用域。
定义Scope
在module
中使用scope
函数可以定义一个作用域,定义Scope时,需要传递一个Qualifier
,当我需要创建Scope实例时,这里的Qualifier
用于帮助我们找到Scope的定义。
val paramModule = module {
scope(named("config")) {
...
}
}
在Scope上定义依赖项
在scope
中,我们可以使用factory
函数以及scoped
函数定义属于这个作用域下的依赖项。
scope(named("config")) {
factory {
File("config.json")
}
scoped {
Configuration(get())
}
}
factory
函数定义了一个生产依赖项的对象工厂,当需要注入依赖项时,Koin就会通过这个对象工厂创建新的实例对象,factory
定义的依赖项不会被缓存起来,每次注入时都会创建新的实例。
scoped
函数可以在当前作用域上定义一个单例的依赖项,当需要注入此依赖项时,Koin会先检查该依赖项在当前作用域上是否已有现成的实例,如果有就直接使用,没有就创建一个缓存起来。
除了上述两种方式外,还有一个single
函数,它用于定义一个在rootScope
上的scoped依赖项。
private val appModule = module {
single {
createOKHTTPClient()
}
}
使用Scope
使用scope时,首先需要获取scope的实例:
val configScope = getKoin().getOrCreateScope(
scopeId = "config-scope",
qualifier = named("config")
)
在Koin
对象上调用getOrCreateScope
用于获取或者创建一个scope实例,其中scopeId
用于缓存这个scope实例时的key,qualifier
则用于在Koin中找到该Scope的定义。
当有了scope实例时,就可以通过它获取依赖项了:
val configFile: File by configScope.inject()
val config: Configuration = configScope.get()
当这个scope不再需要使用时,可以使用colse
函数关闭,此时会销毁当前scope缓存的所有依赖项实例。
configScope.close()
关联作用域
默认情况下我们只能获取在当前作用域下定义的依赖项,使用linkTo
函数可以将其它的作用域关联到当前作用域上,这样,就可以在当前作用域上获取到指定作用域中的依赖项了。
scope(named("common")) {
scoped {
FileUtil()
}
}
configScope.get<FileUtil>() // 未关联前configScope无法获取到FileUtil
val commonScope = getKoin().getOrCreateScope(
scopeId = "common-scope",
qualifier = named("common")
)
configScope.linkTo(commonScope) // 关联后configScope可以获取到FileUtil
另外,所有新定义的作用域都会默认关联rootScope
,也就是说,rootScope
中定义的依赖项,在任何其它作用域上都可以访问到。
在Android组件中使用Scope
Koin为Android组件:Activity
、Fragment
、Service
、ViewModel
等提供了默认的Scope支持,这些Scope会与组件的生命周期进行绑定,在组件创建时初始化,组件销毁时同步销毁。
如需使用组件的Scope,需要先在module中使用组件的类型作为Qualifier
定义Scope:
val activityModule = module {
scope<MainActivity> {
factory {
DataListAdapter()
}
}
}
然后继承对应组件的ScopeXXX版本,比如ScopeActivity。
这样就可以在组件内访问到这个scope中的依赖项了:
class MainActivity : ScopeActivity() {
val adapter: DataListAdapter by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
list.adapter = adapter
...
}
}
传递参数
Koin支持在获取依赖项时传入指定的参数来构造对象实例。
首先,在定义依赖项时,用于创建对象实例的lambda会接收一个ParametersHolder
对象,我们可以通过这个对象获取到传入的用来构造对象实例的参数。
factory { params: ParametersHolder ->
SharedPreferences(get(), params.get())
}
ParametersHolder
为前5个参数提供了解构支持,因此上面的代码也可以这样写:
factory { (name: String) ->
SharedPreferences(get(), name)
}
如果参数的类型并未有其它同类型的依赖项定义时,也可以直接使用get
函数,koin会自动尝试从参数中查找匹配的实例注入,但是并不推荐这样做,这可能会造成一定的误解:
factory {
SharedPreferences(get(), get()/* 在定义的依赖项中找不到时,会尝试从参数中匹配 */)
}
在获取依赖项实例时,我们就需要传递对应的参数,get
函数和inject
函数会接收一个ParametersDefinition
对象,这是一个 () -> ParametersHolder
函数,没错,我们在定义依赖项时使用的ParametersHolder
对象就是这里生成的:
inline fun <reified T : Any> ComponentCallbacks.get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null,
): T
typealias ParametersDefinition = () -> ParametersHolder
使用方式也很简单,在ParametersDefinition
中使用parametersOf
函数将我们要传递的参数打包成一个ParametersHolder
即可。
val mainSp: SharedPreferences by inject() {
parametersOf("main")
}
val prefSp: SharedPreferences by inject() {
parametersOf("pref")
}
⚠️ 需要注意的是,使用
single
或scoped
定义的依赖项,传入不同的参数并不会为每一个参数都生成对应的实例对象。假如我们将上面的SharedPreferences
改为使用single
定义,那么mainSp
和prefSp
会得到同一个main preferences对象。
绑定接口实例
因为默认情况下,Koin使用类型来精确匹配依赖项,因此当我们需要注入一个接口的实例时就会出现一些问题:
interface IService
class ServiceImpl : IService
val appModule = module {
single { ServiceImpl() }
}
上面这样定义时我们只能使用ServiceImpl
类型来获取依赖项实例。
// val service: IService by inject() // 运行时报错
val service: ServiceImpl by inject()
因此,如果我们想将实例注入到一个接口定义的变量上,就需要将对象强转成IService
类型:
val appModule = module {
// 使用as强转
single { ServiceImple() as IService }
// 或者指定为single函数标明类型
single<IService> { ServiceImpl() }
}
但是这时又只能使用IService
来获取依赖项了:
val service: IService by inject()
// val service: ServiceImpl by inject() // 运行时报错
附加类型的绑定可以让一个依赖项实例绑定到不同的类型上,Koin提供了bind
和binds
函数来实现附加类型绑定:
val appModule = module {
single { ServiceImple() } bind IService::class
}
这样ServiceImple
对象实例就可以同时注入给ServiceImple
变量或者IService
变量了。
注入ViewModel
Koin也提供了对Jetpack ViewModel组件的支持,我们可以使用专门的DSL定义ViewModel依赖项实例:
val vmModule = module {
viewModel { MainViewModel(get()) }
}
然后在Activity或者Fragment中使用:
class MainActivity : AppCompatActivity() {
val vm: MainViewModel by viewModel()
// val vm: MainViewModel = getViewModel()
}
class MainFragment : Fragment {
// 通过sharedViewModel可以获取到Fragment宿主的ViewModel
val actVm: MainViewModel by sharedViewModel()
// val actVm: MainViewModel = getSharedViewModel()
}
注入Fragment
Koin提供了KoinFragmentFactory
用于管理注入Fragment。要使用此功能,首先需要在startKoin
中初始化KoinFragmentFactory
的实例:
startKoin {
fragmentFactory()
...
}
然后在module中定义Fragment依赖项:
val fragModule = module {
fragment { MyFragment() }
}
然后就可以在Activity中使用了:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 在当前Activity设置KoinFragmentFactory
setupKoinFragmentFactory()
// 然后就可以通过androidx相关的api设置MyFragment了
supportFragmentManager.beginTransaction()
.replace<MyFragment>(R.layout.activity_main)
.commit()
}
}