FragmentFactory + Koin 实现Fragment依赖注入

Posted fundroid_方卓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FragmentFactory + Koin 实现Fragment依赖注入相关的知识,希望对你有一定的参考价值。

在 《FragmentFactory介绍:构建Fragment的好帮手 》这一文章的结尾处,我对 FragmentFactory 做过如下点评:

“ FragmentFactory 允许开发者使用带参数的构造函数创建 Fragment, 能够在 dagger、koin 等DI 框架的使用场景中发挥作用。”

这之后就有人询问其在 DI 中的具体使用方式。 因此本文以 Koin 为例,介绍如何基于 FragmentFactory 实现 Fragment 的依赖注入

Koin 简介

相信不少朋友对 Koin 都有所了解了,这里再做一个简单介绍:

Koin 是一个轻量级的依赖注入框架,通过 Kotlin 的 DSL 完成配置,全程无反射无代码生成。相对于Dagger/Hilt 来说有以下特点:

  • 易上手:Dagger 的学习曲线陡峭,Hilt 好一些,但仍没有 Koin 简单易用
  • 编译速度快:Koin 没有额外的代码生成,编译速度快
  • 轻量:Dagger/Hilt 在编译后会生大量代码,增加安装包体积,Koin 没有这种烦恼

当然,Koin 也有一些不足,比如缺少编译期检查,需要集中配置不够智能等, 所以综合来说,在大型项目中推荐使用 Dagger/Hilt,而中小项目中 Koin 是一个不错的选择。

Koin DSL

Koin 基于 Kotlin DSL 完成 DI 配置,常用的 DSL 有以下几个:

  • module { } : 类似于 Dagger 的 @Module,提供依赖的单元模块
  • factory { } : 类似于 Dagger 的 @Provide,提供依赖对象,每次都会生成新的实例
  • single { } : 与 factory{} 功能一样,相对于 factory 提供多实例,single 提供单例

需要注意的是 Koin 并不只能用于 android,所以上面的 DSL 在任意 Kotlin 项目中是通用的。 Koin 面向 Android 提供了专用的扩展库及 DSL:

例如下面的扩展库提供了对 FragmentViewModel 的依赖注入能力

implementation "org.koin:koin-androidx-fragment:$koin_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"

上面是对 Koin 的一本基本介绍。 本文还是聚焦在 Koin 中如何使用 FragmentFactoy 实现 Fragment 的依赖注入

创建 Modules

通常情况,在 module{} 中通过 factory{} 提供所需的依赖。

private val fragmentModules = module {
    fragment { HomeFragment() }
    fragment { DetailsFragment(get()) } //通过get()获取依赖的参数
}
private val viewModelsModule = module {
    viewModel { DetailsViewModel(get()) }
}
private val dataModule = module {
    single { DetailsRepository() }
}
//appModules 聚合了所有Koin Modules
val appModules = listOf(fragmentModules, viewModelsModule, dataModule)

fragment{}viewmodel{}koin-androidx 为 Fragment 和 ViewModle 提供的 DSL, 本质就是一个 factory {}:

inline fun <reified T : Fragment> Module.fragment(
    qualifier: Qualifier? = null,
    noinline definition: Definition<T>
): Pair<Module, InstanceFactory<T>> = factory(qualifier, definition)

上面代码的 DetailFragment 通过构造参数依赖了 DetailsViewModelDetailsViewModel 又通过构造参数依赖 DetailsRepository

  inline fun <reified T : Any> get(
        qualifier: Qualifier? = null,
        noinline parameters: ParametersDefinition? = null
    ): T {
        return get(T::class, qualifier, parameters)
    }

get() 是 Koin 中常见的获取依赖的方法,通过泛型 T 对应的 class 作为 Key 查找(或创建)被依赖的对象:

 inline fun <reified T : Any> get(
        qualifier: Qualifier? = null,
        noinline parameters: ParametersDefinition? = null
    ): T {
        return get(T::class, qualifier, parameters)
    }

reified 关键字通过类型推断,帮助 Koin 减少模板代码,这个技巧在 Koin 中被广泛使用。

加载 Modules

applicationonCreate 中,对 Koin 进行初始化,即加载 fragmentModulesviewModelsModule 等各类 Koin 的 Modules。

override fun onCreate() {
    super.onCreate()
    startKoin {
        androidContext(this@App)
        fragmentFactory() // 添加 KoinFragmentFactory
        loadKoinModules(appModules)
    }
}

fragmentFactory() 是一个扩展函数,通过加载 fragmentFactoryModule , 为 Fragment 的构建提供 KoinFragmentFatory

private val fragmentFactoryModule = module {
    single<FragmentFactory> { KoinFragmentFactory() }
}

fun KoinApplication.fragmentFactory() {
    koin.loadModules(listOf(fragmentFactoryModule))
}

KoinFragmentFactory

FragmentTransaction 创建 Fragment 时会调用 KoinFragmentFactoryinstantiate

class KoinFragmentFactory : FragmentFactory() {

    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
        val clazz = Class.forName(className).kotlin
        val instance = getKoin().getOrNull<Fragment>(clazz) //通过 Koin 创建 Fragment
        return instance ?: super.instantiate(classLoader, className)
    }

}

KoinFragmentFactory 通过 Koin 创建 Fragment。 具体是通过 fragment 的 class 作为 key 找到对应 factory ,而这个 factory 就是上面定义的 fragment{...}

看到这里,整个流程就很清楚了:

Koin 通过 KoinFragmentFactory 创建 Fragment,构造函数中允许有参数,可以通过 koin 的依赖注入获取

之前的文章中介绍过,FragmentFactory 需要被设置到 FragmentManager 中使用。那么 KoinFragmentFactory 是何时被设置的呢?

setupKoinFragmentFactory()

override fun onCreate(savedInstanceState: Bundle?) {
    setupKoinFragmentFactory()
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

需要在 Activity#onCreateFragment#onCreate 中调用 setupKoinFragmentFactory(), 将 KoinFragmentFactory 添加到当前 FragmentManager 中。

fun FragmentActivity.setupKoinFragmentFactory(scope: Scope? = null) {
    if (scope == null) {
        supportFragmentManager.fragmentFactory = get()
    } else {
        supportFragmentManager.fragmentFactory = KoinFragmentFactory(scope)
    }
}

需要特别注意,这个调用必须在 super.onCreate 之前完成,因为 super.onCreate 中会进行 fragment 的重建, 此时就需要用到 FragmentFactory 了

当 FragmentFactory 设置完毕后,我们在 MainActivity 中添加 DetailsFragment

supportFragmentManager.beginTransaction()
    .replace(R.id.container, DetailsFragment::class.java, null)
    .commit()

FragmentTransaction 会自动调用 KoinFragmentFactory#instantiate() 创建
DetailsFragment::class.java 对应的 Fragment。

通过断点调试,可以确认,DetailsFragment 中的 viewModel 被成功注入

因为屏幕旋转等造成 Fragment 销毁重建时,viewModel 可以被再次注入,状态不会丢失。

如果在 replace() 中为 Fragment 添加了参数 arguments

val arguments = Bundle().apply { 
    putString("key", "value")
}
supportFragmentManager.beginTransaction()
    .replace(R.id.container, HomeFragment::class.java, arguments)
    .commit()

FragmentTransaction 会在 FragmentFactory 创建完 Fragment 后,通过 setArguments 设置这些参数

不使用 get() 获取依赖

上面例子中 Fragment 通过 get() 获取参数依赖。实际上,Koin 提供了更加简单的方式:

private val fragmentModules = module {
    fragment<HomeFragment>()
    fragment<DetailsFragment>()
}

仅仅声明一个泛型类型,不显示的调用构造函数。

//显示调用构造函数
inline fun <reified T : Fragment> Module.fragment(
    qualifier: Qualifier? = null,
    noinline definition: Definition<T>
): Pair<Module, InstanceFactory<T>> = factory(qualifier, definition)

//不调用构造函数
inline fun <reified T : Fragment> Module.fragment(
    qualifier: Qualifier? = null
): Pair<Module, InstanceFactory<T>> = factory(qualifier) { newInstance(it) } //调用newInstance

Definition<T> 是一个工厂,我们在里面显示调用构造函数,不调用构造函数时,Koin 帮我们调用了 newInstance()

fun <T : Any> Scope.newInstance(kClass: KClass<T>, params: ParametersHolder): T {
    val instance: Any

    val constructor = kClass.java.constructors.firstOrNull()
        ?: error("No constructor found for class '${kClass.getFullName()}'")

    val args =  getArguments(constructor, this, params)

    instance = createInstance(args, constructor)
    return instance as T
}

这里使用了一点反射,获取了构造函数以及构造函数的参数信息

fun getArguments(constructor: Constructor<*>, scope: Scope, parameters: ParametersHolder): Array<Any> {
    val length = constructor.parameterTypes.size
    return if (length == 0) emptyArray()
    else {
        val result = Array<Any>(length) {}
        for (i in 0 until length) {
            val p = constructor.parameterTypes[i]
            val parameterClass = p.kotlin
            result[i] = scope.getOrNull(parameterClass, null) { parameters } ?: parameters.getOrNull(parameterClass) ?: throw NoBeanDefFoundException("No definition found for class '$parameterClass'")
        }
        result
    }
}

最后根据构造参数类型从 Koin 中成功获取依赖。 这种方式省掉了 get(),写法更加简单。

一句话总结

FragmentFactory 允许 Fragment 的构造函数中出现参数,而 Koin 允许构造参数通过依赖注入获取。

以上是关于FragmentFactory + Koin 实现Fragment依赖注入的主要内容,如果未能解决你的问题,请参考以下文章

如何在运行时使用带有参数的 FragmentFactory?

FragmentFactory

如何在 Room MVVM 架构中实现 Koin 依赖注入

如何使用 Koin 依赖注入 (Kotlin) 检索与给定类型匹配的所有实例

找不到 org.koin:koin-core:2.2.2

Koin在KMM与Android Jetpack Compose中的应用