Koin in Android: 更簡單的依賴注入

Dagger 的麻煩

若是還不清楚什麼是依賴注入,那麼請參考以前寫的 Dagger2 in Android(一)通俗基礎開頭部分。若是你不瞭解 Dagger 倒也無妨,本文會進行必定的對比,但僅針對接觸過 Dagger 的同窗,不然大能夠忽略。java

Dagger2 做爲著名優秀的依賴注入框架廣爲流傳,況且仍是 Android 的親爸爸 - Google 在維護,所以相信不少人會將其做爲 Android 開發的首選 DI 框架。Dagger 從入門到放棄必定是不少不少人必經甚至屢次經歷的歷程。android

誠然,Dagger 很強大。但它的學習曲線太過於陡峭,即便好不容易搞清楚了各類註解與概念,也很難適當地運用到項目中。同時對於 Activity 之類重要但卻不能咱們本身初始化的類 Dagger 明顯水土不服。爲此,Google 搞了個 .android 擴展庫來「簡化」使用。我不否定最終確實簡化了代碼,可是這玩意自己就很難度,學習成本堪稱指數級。git

除此以外,.android 擴展庫對於 ViewModel 依然是嚴重的水土不服,甚至 Google 官方 Demo 的實現也是一堆問題。github

Koin 基礎

Koin 是純 Kotlin 編寫的輕量級依賴注入框架,輕量是由於它只使用 Kotlin 的函數解析特性,沒有代理,沒有代碼生成,沒有反射!官方聲稱5分鐘快速上手。隨着 Kotlin 的推廣,Koin 這個後起之秀也得到了愈來愈多的關注。固然它也提供了 implementation "org.koin: koin-java:1.0.0" 擴展庫來支持 java,但本文不會涉及。緩存

不建議新手閱讀 Koin 源碼。做爲 DSL,它大量使用了 Kotlin 的高級特性,例如 inline 函數。相對來講難以理解。架構

使用 Koin 所需的依賴在官方文檔已經說得很明確了。這裏由於使用了 AndroidX 庫,因此引入 org.koin: koin-androidx-viewmodel:2.0.1,事實上它已經包含了 Koin 基礎庫以及 Android 擴展庫,沒有必要手動依賴了。框架

依然是使用廚師與火爐的例子來幫助理解,建立一個廚師 Chef 類,他須要一個火爐 Stoveide

class Stove() {}

class Chef(val stove: Stove) {}
複製代碼

Module

Module 同時充當了 Dagger 裏的 @Inject@Module。Module 是一個容器,它儲存了全部須要注入的對象的實例化方式。換句話說,假如咱們想要在某個類中注入 Stove,那麼就必須在 Module 中定義究竟如何取得或建立 Stove 的實例,這一過程本質是將 Service 插入 Module 圖中。函數

在 Dagger 中,咱們本身編寫的類只需加上 @Inject 就能夠被框架所識別,可是 Koin 要求手動設置。佈局

鑑於 Koin 是一個 DSL,因此 Module 的定義很是簡單,不用囉裏囉嗦的註解,只須要使用 module 函數便可:

val myModule = module{
	factory { Stove() }
}
複製代碼

注意:這裏是定義在 top-level 的,而不是在某個類中。

就這麼簡單,咱們建立了一個 Module 叫作 myModule,而且添加了一個 Service 就是 Stove。添加 Service 有兩個函數分別是 factorysingle。區別在於前者將在每次被須要時都建立(獲取)一個新的實例,也就是說後邊代碼塊將被屢次運行。而 single 會讓 Koin 保留實例用於從此直接返回,相似於 Dagger 中 @Singleton 的做用。

Get

get 用於最終實現注入,顧名思義就是得到一個實例。在 Dagger 中,依賴 @Inject@Component 來實現注入很彆扭。相比之下 get 很是符合常規習慣,在須要獲取實例的地方直接填個 get,Koin 就會根據數據類型自動從上文 Module 中找到匹配的方法取得實例。

val myModule = module{
	factory { Stove() }
	
	factory { Chef(get()) } // 注意這行
}
複製代碼

在正式使用注入以前咱們先新增一個 Chef Service。根據以前的定義,Chef 構造函數中須要傳入一個 Stove,這裏就能夠直接使用 get 獲取。在運行時 Koin 判斷出這裏須要一個 Stove 類型的對象,因而去搜尋全部裝載的 Module 是否有對應的 Service,顯然以前咱們已經定義過了,所以會直接調用 Stove() 來建立一個新的實例並返回,完成了依賴注入流程。

若是所需類型不肯定,或者須要手動指定一個類型,也能夠這麼寫:get<TYPE>()

初始化與使用

前面咱們已經完成了全部準備工做,是否是特別簡單?距離成功注入只有一步之遙啦,如今須要初始化 Koin,一般來講咱們會在 Application.onCreate 中進行。

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            androidLogger(Level.INFO)
            androidContext(this@MyApplication)
            modules(localModule)
        }

    }
}
複製代碼

使用 startKoin 啓動一個全局 Koin。做爲 Android 平臺,還能夠指定 Logger 與 Context。

androidLogger 能夠將 Koin 日誌輸出從默認的 java logger 框架切換到 Android Logcat,更加符合習慣,同時也能夠自定義日誌級別。

androidContext 能夠傳入一個全局 Context,通常來講就採用 Application。做爲 Android 開發對於 Context 必定不陌生,許許多多的地方都須要用到,例如發送廣播或讀取 SharedPreferences。這裏傳入 Context 後至關於在 Module 中插入了 Context Service 定義,在任何須要的地方直接使用 get 就能夠拿到。

最後經過 modules 裝載咱們寫好的 Module。

初始化完成後就能夠在任何地方實現注入了:

class LocalWatchFaceAty : AppCompatActivity() {
	private val chef: Chef = get()
}
複製代碼

OK,官網說的5分鐘入門確實不算誇張~

進一步

Bind

Bind 是一箇中綴函數,能夠用於把一個 Service 關聯到多個類。例如如今有兩個接口:Tool, FlammableStove 實現了他們。顯然若是隻定義1個 Service 是不能同時注入 Stove 和這兩個接口的。這時候就輪到 Bind 大顯身手了:

val myModule = module{
	factory { Stove() } bind Tool::class bind Flammable::class // <- here! factory { Chef(get()) } } 複製代碼

這麼一來,下面的三個注入都是合法的,並都會獲得一個 Stove 實例:

val chef: Chef = get()
val tool:Tool = get()
val flammable:Flammable = get()
複製代碼

Scope

Scope 用於控制對象在 Koin 內的生命週期。事實上,前面所講的 singlefactory 都是 scope。

  • single 建立的對象在整個容器的生命週期內都是存在的,所以任意地方注入都是同一實例。
  • factory 每次都建立新的對象,所以它不被保存,也不能共享實例。

定義 Scope 比較簡單:

val myModule = module{
	scope(named("MY_SCOPE")){
        scoped {
            Stove()
        }
    }
}
複製代碼

可是使用起來就比較麻煩了,咱們須要建立或關閉 scope,畢竟 Kolin 怎麼會知道你究竟想實現怎樣的生命週期呢?

// 若是存在則直接獲取,不然建立 scope
val scope = getKoin().getOrCreateScope("myScope", named("MY_SCOPE"))
val stove1: Stove = scope.get()
val stove2: Stove = scope.get()
scope.close()
複製代碼

這裏首先獲得了一個 scope 實例,而後進行注入,最後關閉 scope。那麼在同一個 scope 中注入的實例是相同的。例如 stove1stove2 其實是同一個實例。當 scope 被關閉時其緩存會被清空,天然下一次從新建立後會注入新的對象。

注意區分一點,定義 Scope 時使用的叫作 Qualifier,經過 named 能夠用字符串包裝。在建立 scope 時須要經過 Qualifier 關聯到定義,並同時給一個字符串類型的 idid 僅在運行時使用。能夠類比成 Android 的佈局文件的 View id 與實際變量名的關係。咱們須要經過 View id 來獲取實例並賦值給變量保存,變量名與 View id 沒有必然的關係。


在 Android 中咱們常常須要以 Activity 爲單位建立 scope,爲了簡化使用,Koin 提供了 Android 擴展庫。在 ActivityFragment 中,能夠直接使用 currentScope 變量來表示當前 scope,他會被自動建立,並綁定到 Android 組件的生命週期。

class LocalWatchFaceAty : AppCompatActivity() {
	private val stove: Stove by currentScope.inject()
}
複製代碼

除了以前使用的 get,還能夠像這樣使用 inject 實現懶加載。

定義 scope 也變得簡單。以前咱們使用字符串做爲限定符定義了 scope,如今直接使用類做爲限定符:

val myModule = module{
	scope(named<LocalWatchFaceAty>()){
        scoped {
            Stove()
        }
    }
}
複製代碼

ViewModel

ViewModel 能夠說是 Android 架構組件發佈後最流行的部分了,幸運的是 Kolin 對其作了很是方便的適配。對於 ViewModel 類直接使用 viewModel 來定義 Service:

val localModule = module {
	viewModel {
        KitchenViewModel()
    }
}
複製代碼

ActivityFragment 中直接使用 by viewModel()getViewModel() 來注入。

class LocalWatchFaceAty : AppCompatActivity() {
	private val vm: KitchenViewModel by viewModel()
}
複製代碼

這樣一來獲得的 ViewModel 能夠自動與 UI 生命週期關聯。而若是使用傳統的 get 只能獲得實例但沒有任何關聯,失去了 ViewModel 最重要的做用。

總結

能夠明顯感覺到,Koin 小巧精美,上手難度低,與現代化架構技術很是協調。使用起來符合常規習慣,不要被迫學習一堆概念與複雜的模式。

事實上,Koin 還有更多的高級功能,例如動態加載 Module、本地配置項讀取等,也都很簡單,經過官方文檔能夠快速瞭解。

相關文章
相關標籤/搜索