当Koin撞上ViewModel

写在前面

在上一篇《当Dagger2撞上ViewModel》的文章里,我简单阐述了Dagger-ViewModel这样的写法以简化Dagger2的使用,当时有评论推荐我使用Koin,当我尝试以后,发现Koin上手很是容易,实际上更加符合个人《MVVM With Kotin》框架,并且其也对ViewModel组件进行了支持,所以我在PaoNet示例之中将Dagger2替换为了Koin,能够在如下连接中查看相关代码。html

本文示例:github.com/ditclear/MV…java

完整示例:github.com/ditclear/Pa…android

在这之间的迁移过程当中,基本上没遇到什么大的问题,但也由于Koin上手比较容易,只有寥寥的几篇博客介绍了它的使用方法,对其原理介绍的还没看到。但做为开发者,若是只知道使用而不去了解它的内部实现,那么便会只知其形,而不解其意,当遇到问题会花费更多的时间去填坑,也浪费了一次提高自我能力的机会。git

所以,在这里写下本身对Koin的一些心得体会,但愿后来人能少走些弯路。github

What is Koin?

A pragmatic lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin using functional resolution only: no proxy, no code generation, no reflection!app

Koin is a DSL, a lightweight container and a pragmatic API.框架

Koin 是为Kotlin开发者提供的一个实用型轻量级依赖注入框架,采用纯Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射。ide

Koin 是一个DSL,一个轻量级容器,也更加实用。函数

开始以前,咱们首先要知道几点知识。工具

Koin使用了不少的内联函数,它的做用简单来讲就是方便进行类型推导,能具体化类型参数

inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
    println(membersOf<StringBuilder>().joinToString("\n"))
}
复制代码
  • module { } - create a Koin Module or a submodule (inside a module)

相似于Dagger的@Module,里面提供所需的依赖

  • factory { } - provide a factory bean definition

相似于Dagger的@Provide,提供依赖,每次使用到的时候都会生成新的实例

  • single { } - provide a bean definition

同factory,区别在于其提供的实例是单例的

  • get() - resolve a component dependency
/** 可经过name或者class检索到对应的实例 * Retrieve an instance from its name/class * @param name * @param scope * @param parameters */
    inline fun <reified T : Any> get( name: String = "", scope: Scope? = null, noinline parameters: ParameterDefinition = emptyParameterDefinition()
    ): T = instanceRegistry.resolve(
        InstanceRequest(
            name = name,
            clazz = T::class, scope = scope, parameters = parameters
        )
    )
复制代码

若是你想继续了解Koin,能够查看如下连接

官网:insert-koin.io/

Koin-Dsl:insert-koin.io/docs/1.0/qu…

Koin文档:insert-koin.io/docs/1.0/do…

快速上手

首先,添加Koin-ViewModel的依赖,注意须要对是否AndroidX版本进行区分

// Add Jcenter to your repositories if needed
repositories {
    jcenter()
}
dependencies {
    // 非AndroidX 添加
    implementation 'org.koin:koin-android-viewmodel:1.0.1'
    // AndroidX 添加
    implementation 'org.koin:koin-androidx-viewmodel:1.0.1'
}
复制代码

而后定义你所需的依赖定义的集合

val viewModelModule = module {
    viewModel { PaoViewModel(get<PaoRepo>()) }
    //or use reflection
// viewModel<PaoViewModel>()

}

val repoModule = module {

    factory  <PaoRepo> { PaoRepo(get(), get()) }
    //其实就是
    //factory <PaoRepo> { PaoRepo(get<PaoService>(), get<PaoDao>()) }

	
}

val remoteModule = module {

    single<Retrofit> {
        Retrofit.Builder()
                .baseUrl(Constants.HOST_API)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build()
    }

    single<PaoService> { get<Retrofit>().create(PaoService::class.java) }
}


val localModule = module {

    single<AppDatabase> { AppDatabase.getInstance(androidApplication()) }

    single<PaoDao> { get<AppDatabase>().paoDao() }
}

//当须要构建你的ViewModel对象的时候,就会在这个容器里进行检索
val appModule = listOf(viewModelModule, repoModule, remoteModule, localModule)
复制代码

在你的Application中进行初始化

class PaoApp : Application() {


    override fun onCreate() {
        super.onCreate()

        startKoin(this, appModule, logger = AndroidLogger(showDebug = BuildConfig.DEBUG))
    }


}
复制代码

最后注入你的ViewModel

class PaoActivity : AppCompatActivity() {
    //di
    private val mViewModel: PaoViewModel by viewModel()
    
    //...
    
    fun doSth(){
        
        mViewModel.doSth()
     
    }
}
复制代码

Koin是怎么进行注入的?

咱们先撇开Koin的原理不谈,不用任何注入框架,这个时候,咱们建立一个实例,就须要一步步的去建立其所需的依赖。

val retrofit = Retrofit.Builder()
        .baseUrl(Constants.HOST_API)
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create())
        .build()
val remote = retrofit.create(PaoService::class.java)
val database = AppDatabase.getInstance(applicationContext)
val local= database.paoDao()
val repo = PaoRepo(remote, local)
val mViewModel = PaoViewModel(repo)
复制代码

当建立多个ViewModel的时候,这样子的模板化的代码无疑会拖慢开发效率。也正由于这些都是模板化的代码,建立方式都大致一致,所以便给了咱们一种可能——依赖检索

假设咱们有一个全局的容器,里面提供了应用全部所需实例的构造方式,那么当咱们须要新建实例的时候,就能够直接从这个容器里面获取到它的构造方式而后拿到所需的依赖,从而构造出所需的实例。

Koin要作的也就是这个。

当在Application中运行如下代码时

startKoin(this, appModule, logger = AndroidLogger(showDebug = BuildConfig.DEBUG))
复制代码

Dagger-ViewModel所作的事情同样,Koin也会提供一个全局容器,将全部的依赖构造方式转换成BeanDefinition进行注册,这是一个HashSet,其名为definitions

definitions

BeanDefinition得定义以下所示:

/** * Bean definition * @author - Arnaud GIULIANI * * Gather type of T * defined by lazy/function * has a type (clazz) * has a BeanType : default singleton * has a canonicalName, if specified * * @param name - bean canonicalName * @param primaryType - bean class * @param kind - bean definition Kind * @param types - list of assignable types * @param isEager - definition tagged to be created on start * @param allowOverride - definition tagged to allow definition override or not * @param definition - bean definition function */
data class BeanDefinition<out T>(
    val name: String = "",
    val primaryType: KClass<*>,
    var types: List<KClass<*>> = arrayListOf(),
    val path: Path = Path.root(),
    val kind: Kind = Kind.Single,
    val isEager: Boolean = false,
    val allowOverride: Boolean = false,
    val attributes: HashMap<String, Any> = HashMap(),
    val definition: Definition<T>
    )
复制代码

咱们主要看name以及primaryType,还记得get()关键字么?这两个即是依赖检索所需的key。

还有一个 definition: Definition<T>,它的值表明了其构造方式来源于那个module,对应前文的viewModelModulerepoModuleremoteModulelocalModule,经过它能够反向推导该实例须要哪些依赖。

明白了这些,咱们再来到获取ViewModel实例的地方,看看viewModel()方法是怎么作的。

class PaoActivity : AppCompatActivity() {
    //di
    private val mViewModel: PaoViewModel by viewModel()
   
}
复制代码

viwModel()的具体实现

/** * Lazy getByClass a viewModel instance * * @param key - ViewModel Factory key (if have several instances from same ViewModel) * @param name - Koin BeanDefinition name (if have several ViewModel beanDefinition of the same type) * @param parameters - parameters to pass to the BeanDefinition */
inline fun <reified T : ViewModel> LifecycleOwner.viewModel( key: String? = null, name: String? = null, noinline parameters: ParameterDefinition = emptyParameterDefinition()
) = viewModelByClass(T::class, key, name, null, parameters)
复制代码

默认经过Class进行懒加载,再来看看viewModelByClass()方法

/** * 获取viewModel实例 * */
fun <T : ViewModel> LifecycleOwner.getViewModelByClass( clazz: KClass<T>, key: String? = null, name: String? = null, from: ViewModelStoreOwnerDefinition? = null, parameters: ParameterDefinition = emptyParameterDefinition()
): T {
    Koin.logger.debug("[ViewModel] ~ '$clazz'(name:'$name' key:'$key') - $this")

    val vmStore: ViewModelStore = getViewModelStore(from, clazz)
	//**关键在于这里**
    val viewModelProvider =
        makeViewModelProvider(vmStore, name, clazz, parameters)
	//ViewModel组件获取ViewModel实例
    return viewModelProvider.getInstance(key, clazz)
}

/** * 构建对应的ViewModelProvider * */
private fun <T : ViewModel> makeViewModelProvider( vmStore: ViewModelStore, name: String?, clazz: KClass<T>, parameters: ParameterDefinition ): ViewModelProvider {
    return ViewModelProvider(
        vmStore,
        object : ViewModelProvider.Factory, KoinComponent {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                //在definitions中进行查找
                return get(name ?: "", clazz, parameters = parameters)
            }
        })
}
复制代码

文字描述一下其中的过程:

好比如今须要一个PaoViewModel的实例,那么经过clazz为Class<PaoViewModel>的key在definitions中进行查找

find in definitions

最后查到有一个PaoViewModelBeanDefinition,经过注册过的 definition: Definition<T>找到其构造方式的位置。

发现ViewModel的构造方式

当经过PaoViewModel(get())的构造方式去构造PaoViewModel实例的时候,发现又有一个get<PaoRepo>(),而后就是再重复前面的逻辑,一直到生成ViewModel实例为止。

这些经过Koin提供的Debug工具,能够在LogCat中很直观的看到构建过程。

logcat

并且报错更加友好,当你有什么依赖没有定义的时候,Koin也会比Dagger更好的提醒你。

写在最后

咱们能够再跟Dagger-ViewModel比较一下。

二者构建实例的方法实际上是同样的。

不一样之处在于Koin须要咱们定义好各个依赖它的构造方式,当咱们须要具体实例的时候,它会去definitions容器里检索,逐步构造。

而Dagger-ViewModel则是经过注解,帮咱们在编译期间就找到依赖,生成具体的构造方法,免去了运行时去检索的步骤。

若是说把怎么样进行注入做为一道考题,那么这二者均可以算是正确答案。

就实用性而言,我选择Koin,它是纯Kotlin代码,上手简单,并且没必要在编译期间生成代码,减小了编译时间,报错也比Dagger2更加友好。再者,Koin还支持在构建过程当中加入参数,是更适合个人依赖注入框架。

不过,Koin中有不少的内联函数和Dsl语法,源码中不少都没有明确的写明泛型,很容易把人看的云里雾里的,这也算是其缺点吧。

其它

Koin官网:insert-koin.io/

本文示例:github.com/ditclear/MV…

完整示例:github.com/ditclear/Pa…

《使用Kotlin构建MVVM应用程序系列》 :www.jianshu.com/c/50336d57e…

简书:www.jianshu.com/p/80c4852cb…

相关文章
相关标签/搜索