依赖注入(十一)—— Koin

2,351 阅读9分钟

官网:insert-koin.io/

Koin是一个为Kotlin设计的轻量级依赖注入框架(依赖检索容器)。

关键词:DSLLight、无代码生成。


引入依赖

koin是为Kotlin语言设计的框架,因此在多数使用到Kotlin的地方都可以使用。同时,Koin还为Android、Android Compose、Ktor提供了专用版本。

本节仅介绍Koin在Android中的引入方式,其它使用场景请参考官方文档。

  1. 在项目根级gradle脚本中添加maven仓库

    repositories {
        ...
        mavenCentral()
    }
    
  2. 在模块级别添加对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提供了支持注入依赖的扩展,因此在ActivityFragmentService等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组件:ActivityFragmentServiceViewModel等提供了默认的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")
}

⚠️ 需要注意的是,使用singlescoped定义的依赖项,传入不同的参数并不会为每一个参数都生成对应的实例对象。假如我们将上面的SharedPreferences改为使用single定义,那么mainSpprefSp会得到同一个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提供了bindbinds函数来实现附加类型绑定:

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()
    }
}