官网: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()
    }
}