當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!markdown

Koin is a DSL, a lightweight container and a pragmatic API.app

Koin 是爲Kotlin開發者提供的一個實用型輕量級依賴注入框架,採用純Kotlin 語言編寫而成,僅使用功能解析,無代理、無代碼生成、無反射。框架

Koin 是一個DSL,一個輕量級容器,也更加實用。ide

開始以前,咱們首先要知道幾點知識。函數

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…

相關文章
相關標籤/搜索