當Koin撞上ViewModel

ditclear發表於2018-12-23

寫在前面

在上一篇《當Dagger2撞上ViewModel》的文章裡,我簡單闡述了Dagger-ViewModel這樣的寫法以簡化Dagger2的使用,當時有評論推薦我使用Koin,當我嘗試之後,發現Koin上手非常容易,實際上更加符合我的《MVVM With Kotin》框架,而且其也對ViewModel元件進行了支援,因此我在PaoNet示例之中將Dagger2替換為了Koin,可以在以下連結中檢視相關程式碼。

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

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

在這之間的遷移過程中,基本上沒遇到什麼大的問題,但也因為Koin上手比較容易,只有寥寥的幾篇部落格介紹了它的使用方法,對其原理介紹的還沒看到。但作為開發者,如果只知道使用而不去了解它的內部實現,那麼便會只知其形,而不解其意,當遇到問題會花費更多的時間去填坑,也浪費了一次提升自我能力的機會。

因此,在這裡寫下自己對Koin的一些心得體會,希望後來人能少走些彎路。

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!

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

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

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…

相關文章