前言:依賴注入淺談
Dagger2的困境
對(duì)于依賴注入(Dependency Injection饿肺,簡(jiǎn)稱DI)來講蒋困,它并非是一個(gè)新鮮的詞匯,實(shí)際上敬辣,它很早就被提出并且應(yīng)用在了企業(yè)級(jí)的web應(yīng)用開發(fā)當(dāng)中雪标,比如Spring。
在Android開發(fā)領(lǐng)域內(nèi)溉跃,毫無疑問村刨,Google大名鼎鼎的 Dagger2 是依賴注入框架的首選工具庫(kù),它非常優(yōu)秀撰茎,Github上數(shù)以萬(wàn)計(jì)的star是最強(qiáng)力的佐證嵌牺,但是缺點(diǎn)也很明顯,那就是:
復(fù)雜龄糊。
這最直接導(dǎo)致了 極為高昂的學(xué)習(xí)成本——你可以不認(rèn)為它難逆粹,但你要承認(rèn)學(xué)習(xí)Dagger2比你學(xué)習(xí)其他庫(kù)所花費(fèi)的時(shí)間要多得多。
我的感受
我是卻把清梅嗅炫惩,一個(gè)Android開發(fā)者僻弹,此外在業(yè)余時(shí)間,我喜歡學(xué)習(xí)總結(jié)分享他嚷。
對(duì)于Dagger奢方,我曾經(jīng)花了6篇博客的篇幅去闡述了我的理解(請(qǐng)參考https://blog.csdn.net/column/details/17168.html),于現(xiàn)在來看爸舒,這些文章還存在一些瑕疵蟋字,但對(duì)于當(dāng)時(shí)的我來說,我盡力去闡述了扭勉,但是還有問多評(píng)論這樣說:
文章講的很好鹊奖,但是我還是遇到了一些問題,請(qǐng)教博主........
Dagger太復(fù)雜了涂炎!一方面它意味著更靈活忠聚,更多強(qiáng)大功能的支持,同時(shí)它也意味著——不夠簡(jiǎn)潔唱捣。
你需要配置Module两蟀,需要配置Component,然后在你需要注入的容器中震缭,初始化Component然后注入進(jìn)來——這一切的前提是赂毯,你還得先正確地進(jìn)行依賴配置,以保證編譯器不會(huì)爆出一大串Error。
當(dāng)然党涕,對(duì)于其中的一些問題烦感,世界上那些最頂尖的工程師們已經(jīng)做出了更好的解決方案,dagger.android橫空出世膛堤,對(duì)于Activity和Fragment容器手趣,開發(fā)者再也不需要初始化Component的模版代碼了。
dagger.android并沒有解決 學(xué)習(xí)成本高昂的問題——相反肥荔,它需要在熟悉Dagger2的基礎(chǔ)上繼續(xù)深入绿渣,反而增加了學(xué)習(xí)成本;但不可否認(rèn)燕耿,它依然是目前Android開發(fā)中依賴注入工具選型中的首選怯晕。
我也曾經(jīng)一度這樣想過,對(duì)于依賴注入這個(gè)技能點(diǎn)缸棵,熟練dagger.android的使用舟茶,閱讀源碼并掌握原理 已經(jīng)足夠。
——但Kotlin時(shí)代到來了堵第。
Kotlin時(shí)代
最近個(gè)人在嘗試構(gòu)建適合自己的 Kotlin的 MVVM 吧凉,在依賴注入框架的選型上,我最終選擇了 Kodein 踏志。
這是一個(gè)非常輕量級(jí)的DI框架阀捅,同樣,它也被《Kotlin in Action》一書所推薦针余,相比于配置繁瑣的Dagger饲鄙,它的配置過程更清晰且簡(jiǎn)單,并且圆雁,這個(gè)庫(kù)的源碼也是 Kotlin 的忍级。
有同學(xué)說,雖然 Dagger 配置很繁瑣伪朽,但 dagger.android 已經(jīng)大大減少了模板代碼轴咱,為什么不使用它呢?
確實(shí)如此烈涮,但Dagger終究是通過編譯器自動(dòng)生成 Java 代碼的庫(kù)朴肺,這實(shí)在不夠 Kotlin,于我個(gè)人來講坚洽,Dagger并非最優(yōu)選戈稿。
Kodein:入門篇
雖然 Kodein 全名為 KOtlin DEpendency INjection,但Kodein并不是一個(gè)真正的依賴注入框架讶舰。 他們的官方文檔將其稱作依賴檢索容器鞍盗。
下面是Kodein的官方文檔:
Kodein官方文檔:Getting started with Kodein DI
Kodein官方文檔 for Android:Kodein DI on Android
本文的主旨是需了,讓開發(fā)者更快入門Kodein和理解其思想,如果您想更深入學(xué)習(xí)它橡疼,官方文檔是你不二的選擇。
在這里我推薦官方文檔的原因庐舟,一是欣除,英文文檔對(duì)一些專業(yè)詞匯和思想,描述的更清晰準(zhǔn)確挪略;第二就是历帚,關(guān)于Kodein目前國(guó)內(nèi)還沒有任何相關(guān)中文資料....
讓我們開始使用它吧。
1.添加依賴
在Module級(jí)別的build.gradle中添加Kodein最新的依賴:
// 基礎(chǔ)組件
implementation 'org.kodein.di:kodein-di-generic-jvm:5.2.0'
// Android擴(kuò)展組件
implementation 'org.kodein.di:kodein-di-framework-android-core:5.2.0'
// support擴(kuò)展組件杠娱,我的項(xiàng)目中用到了v4包的Fragment挽牢,因此我需要它
implementation 'org.kodein.di:kodein-di-framework-android-support:5.2.0'
如果依賴不成功,你需要把kodein的maven倉(cāng)庫(kù)顯式地聲明出來:
allprojects {
repositories {
google()
jcenter()
// maven for kodein
maven { url 'https://dl.bintray.com/kodein-framework/Kodein-DI/' }
}
}
2.如何使用它摊求?
我們必須嘗試回憶Dagger2的核心思想:它是將依賴通過Module管理提供禽拔,然后交給Component注入給Activity等容器。
Kodein并不是一個(gè)真正的依賴注入框架室叉。 他們的官方文檔將其稱作依賴檢索容器睹栖。
這是我文中第二次聲明,Kodein和Dagger的核心思想有所不同茧痕,其原理是——將依賴交給一個(gè) Kodein容器
野来,然后將Kodein容器
交給Activity
,Activity
中所需要的依賴通過委托給Kodein容器
注入踪旷。
好了好了曼氛,我知道這段話很抽象,我們來看一個(gè)案例令野,它將展示如何把一個(gè)SQLiteDatabase
對(duì)象通過Kodein
進(jìn)行依賴注入舀患。
首先我們先聲明一個(gè)Kodein容器:
val kodein = Kodein {
bind<Database>() with singleton { SQLiteDatabase() }
}
Kodein提供了對(duì)DSL的強(qiáng)大支持,正如你所看到的气破,我們可以將SQLiteDatabase
對(duì)象的實(shí)例化過程构舟,放在Kodein 開頭的{ }
中。
bind<T>()
意味著你聲明將一個(gè)類型為T
的依賴放入了Kodein容器進(jìn)行綁定(bind)堵幽。作為一個(gè)非常重的對(duì)象狗超,SQLiteDatabase
更應(yīng)該保持單例,所以我們?cè)趯?duì)其實(shí)例化的方式上朴下,選擇了singleton { }
努咐。
相比于dagger
,這種配置方式實(shí)在太清晰了——沒有@Inject
,沒有@Providers
殴胧,沒有@Component
,你只需要通過Kotlin所支持的DSL渗稍,就能輕松完成各種方式依賴的綁定佩迟。
3.有哪些綁定方式呢?
本文不是Kodein的文檔竿屹,但是我認(rèn)為花一些篇幅講解這些是有必要的报强,Kodein共提供了provider, singleton, factory, multiton, instance
等等多種方式的綁定。
singleton
正如上文描述過的拱燃,這種方式會(huì)實(shí)例化一個(gè)單例對(duì)象秉溉,該單例對(duì)象將會(huì)在第一次使用時(shí)通過單例函數(shù)進(jìn)行創(chuàng)建,該函數(shù)不帶參數(shù)并返回綁定類型的對(duì)象(例如()→T
)碗誉。
示例代碼召嘶,該對(duì)象將會(huì)在第一次被調(diào)用時(shí),通過調(diào)用該函數(shù)哮缺,將對(duì)象進(jìn)行生成并返回弄跌,該函數(shù)有且僅會(huì)有一次被調(diào)用:
val kodein = Kodein {
bind<DataSource>() with singleton { SqliteDS.open("path/to/file") }
}
provider
和singleton不同,該函數(shù)每次都會(huì)被調(diào)用并返回對(duì)應(yīng)的依賴尝苇。
示例代碼,每次都會(huì)調(diào)用該函數(shù)铛只,返回一個(gè)新生成的對(duì)象:
val kodein = Kodein {
bind<Die>() with provider { RandomDie(6) }
}
factory
和provider
很相似,每次都會(huì)調(diào)用該函數(shù)糠溜,返回一個(gè)新生成的對(duì)象格仲,不同的是,factory
函數(shù)接受已定義類型的參數(shù)并返回綁定類型的對(duì)象(例如诵冒,(A)→T
)凯肋。
示例代碼,根據(jù)參數(shù)sides
的不同,每次都會(huì)返回一個(gè)新的Die
:
val kodein = Kodein {
bind<Die>() with factory { sides: Int -> RandomDie(sides) }
}
還有更多...
還有更多汽馋,請(qǐng)參考官方文檔中關(guān)于綁定聲明方式的說明侮东。
4.進(jìn)行依賴注入
我們已經(jīng)通過不同的方式,完成了依賴綁定豹芯,接下來悄雅,我們就可以進(jìn)行依賴注入了。
還記得我已經(jīng)提了兩遍的話嗎铁蹈,Kodein是一個(gè)依賴檢索容器宽闲。
我們把綁定的依賴交給Kodein
容器,然后我們把這個(gè)容器交給Activity
握牧,Activity
就可以從容器中取出這些依賴了容诬。
稍微有點(diǎn)不同的是,取出依賴的方式是通過kotlin的屬性委托
:
class Presenter(val kodein: Kodein) { // presenter擁有kodein容器
private val db: Database by kodein.instance() // 通過屬性委托沿腰,即可依賴注入
private val rnd: Random by kodein.instance()
}
現(xiàn)在览徒,我們就可以直接對(duì)這些對(duì)象進(jìn)行引用了,have fun颂龙!
Kodein:實(shí)戰(zhàn)篇
拋開項(xiàng)目架構(gòu)談工具都是刷流氓习蓬。
上述內(nèi)容僅僅提供了對(duì)Kodein的簡(jiǎn)單了解纽什,實(shí)際上,無論是是dagger2
還是kodein
躲叼,會(huì)寫demo 和 在項(xiàng)目中應(yīng)用 完全是天差地別芦缰。
如果只是簡(jiǎn)單的API介紹,這篇文章也許更早就出來了枫慷,事實(shí)上让蕾,我在嘗試構(gòu)建 Kotlin的 MVVM 項(xiàng)目時(shí),將Kodein加了進(jìn)去流礁,并不斷進(jìn)行調(diào)整——直到現(xiàn)在涕俗,我對(duì)它有了更清晰的一些認(rèn)識(shí)以及理解罗丰。
當(dāng)然神帅,它們不一定就是對(duì)的,或者說萌抵,不一定就是適合你的找御,但我希望,我的這次實(shí)踐绍填,能夠讓你對(duì)Kodein有更深度的了解:
MVVM-Rhine:The MVVM Architecture in Android.
MVVM-Rhine霎桅,是我目前在嘗試探索的mvvm開發(fā)架構(gòu)(Rhine:萊茵河),目前處于摸索和開發(fā)期讨永,歡迎參考并提出建議滔驶。
1.定制Application
在一個(gè)Android項(xiàng)目中,很多依賴都需要保持單例卿闹,這樣能夠保證合理的資源規(guī)劃揭糕,比如,Retrofit的實(shí)例化锻霎,比如Gson對(duì)象的實(shí)例化著角,這里我們直接在Application中進(jìn)行配置:
open class RhineApplication : Application(), KodeinAware {
override val kodein: Kodein = Kodein.lazy {
}
}
KodeinAware
是一個(gè)接口,它意味著旋恼,實(shí)現(xiàn)該接口的對(duì)象都會(huì)持有一個(gè)Kodein容器:
interface KodeinAware {
/**
* A Kodein Aware class must be within reach of a [Kodein] object.
*/
val kodein: Kodein
val kodeinContext: KodeinContext<*> get() = AnyKodeinContext
val kodeinTrigger: KodeinTrigger? get() = null
}
RhineApplication
實(shí)現(xiàn)了KodeinAware接口吏口,并實(shí)例化了一個(gè)Kodein
容器,接下來我們要做的冰更,就是把對(duì)應(yīng)的依賴裝進(jìn)Kodein
容器中产徊。
我們定義了一個(gè)httpClientModule
的頂層屬性
以聲明Retrofit相關(guān):
const val HTTP_CLIENT_MODULE_TAG = "httpClientModule"
const val TIME_OUT_SECONDS = 20
val httpClientModule = Kodein.Module(HTTP_CLIENT_MODULE_TAG) {
bind<Retrofit.Builder>() with singleton { Retrofit.Builder() }
bind<OkHttpClient.Builder>() with singleton { OkHttpClient.Builder() }
bind<Retrofit>() with singleton {
instance<Retrofit.Builder>() // 委托給了bind<Retrofit.Builder>()函數(shù)
.baseUrl(APIConstants.BASE_API)
.client(instance()) // 委托給了 bind<OkHttpClient>() 函數(shù)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()
}
bind<OkHttpClient>() with singleton {
instance<OkHttpClient.Builder>() // 委托給bind<OkHttpClient.Builder>()函數(shù)
.connectTimeout(
TIME_OUT_SECONDS.toLong(),
TimeUnit.SECONDS)
.readTimeout(
TIME_OUT_SECONDS.toLong(),
TimeUnit.SECONDS)
.addInterceptor(HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY))
.build()
}
bind<Gson>() with singleton { Gson() }
}
Retrofit聲明好了,我們?cè)俾暶鲗?duì)應(yīng)的APIService:
const val SERVICE_MODULE_TAG = "serviceModule"
val serviceModule = Kodein.Module(SERVICE_MODULE_TAG) {
bind<UserService>() with singleton {
// Retrofit對(duì)象的獲取已經(jīng)在httpClientModule中聲明好了
instance<Retrofit>().create(UserService::class.java)
}
bind<ServiceManager>() with singleton {
ServiceManager(instance()) // userService的獲取方式已經(jīng)聲明
}
}
// 目前ServiceManager只有User相關(guān)的API接口蜀细,可后續(xù)慢慢追加
data class ServiceManager(val userService: UserService)
當(dāng)然囚痴,這些依賴的綁定都依賴于項(xiàng)目架構(gòu)
,比如审葬,我的項(xiàng)目用到了RxCache
深滚,我也聲明了對(duì)應(yīng)的cacheModule
:
val CACHE_MODULE_TAG = "CacheModule"
val cacheModule = Kodein.Module(CACHE_MODULE_TAG) {
bind<RxCache>() with singleton {
RxCache.Builder()
.persistence(ContextCompat.getExternalCacheDirs(instance())[0], GsonSpeaker())
}
}
這些依賴最終都統(tǒng)一交給RhineApplication
,在我的項(xiàng)目中奕谭,它大概是這樣的:
open class RhineApplication : Application(), KodeinAware {
override val kodein: Kodein = Kodein.lazy {
bind<Context>() with singleton { this@RhineApplication }
import(androidModule(this@RhineApplication))
import(androidSupportModule(this@RhineApplication))
import(serviceModule)
import(cacheModule)
import(rxModule)
import(httpClientModule)
}
}
2.定制Activity或者Fragment
全局的依賴交給了RhineApplication
,如果對(duì)于一個(gè)Activity,它可能還有其他的依賴需要注入痴荐,這意味著血柳,我們需要:
- 1.
RhineApplication
級(jí)別的Kodein容器 - 2.
Activity
級(jí)別的Kodein容器,它包含僅Activity所需依賴
這很簡(jiǎn)單生兆,我們只需要extend
和 import
:
class MainActivity : BaseActivity() 难捌,KodeinAware {
private val parentKodein by closestKodein() // 1
override val kodein: Kodein by retainedKodein {
extend(parentKodein, copy = Copy.All) // 2
import(mainKodeinModule) // 3
bind<MainActivity>() with instance(this@MainActivity) // 4
}
// 注入MainNavigator控制Activity的視圖導(dǎo)航
private val navigator: MainNavigator by instance()
// 注入MainViewModel管理業(yè)務(wù)數(shù)據(jù)
private val mainViewModel: MainViewModel by instance()
}
這里的Activity代碼僅方便讀者理解,實(shí)際代碼因架構(gòu)設(shè)計(jì)有一定偏差:
1.closestKodein()
函數(shù)返回了相鄰上層
的一個(gè)Kodein容器鸦难,對(duì)于Activity
來說根吁,它返回的是Application
層級(jí)的Kodein
容器。
2.通過extend()
函數(shù)合蔽,我們將Application層級(jí)的Kodein容器也放在了Activity
的kodein容器中击敌,這樣Activity
就能從上層的Kodein容器取出對(duì)應(yīng)依賴(俄羅斯套娃?)拴事,比如網(wǎng)絡(luò)請(qǐng)求的service相關(guān)等等沃斤。
3.類似Application
的注入方式一樣,我定義了一個(gè)mainKodeinModule
,以存放MainActiviy
所需依賴的綁定函數(shù),類似dagger中的@Scope
,其中scoped(AndroidComponentsWeakScope)
保證了Activity
級(jí)別的局部單例
刃宵,:
val MAIN_MODULE_TAG = "MAIN_MODULE_TAG"
val mainKodeinModule = Kodein.Module(MAIN_MODULE_TAG) {
// 省略很多代碼...
bind<HomeFragment>() with scoped(AndroidComponentsWeakScope).singleton {
HomeFragment()
}
bind<MainViewModel>() with scoped(AndroidComponentsWeakScope).singleton {
// 這里需要MainActivity衡瓶,請(qǐng)參考下文中4的講解
instance<MainActivity>().viewModel(MainViewModel::class.java)
}
}
4.正如上文看到的,一些對(duì)象的實(shí)例化需要Context
的上下文對(duì)象牲证,我們通過bind<MainActivity>() with instance(this@MainActivity)
完成MainActivity
的綁定哮针。
Kodein:小結(jié)
能看到這里的,基本都是真愛了坦袍,實(shí)際上十厢,相比于Dagger,Kodein的學(xué)習(xí)成本更低键闺,代碼更簡(jiǎn)潔寿烟,配置更簡(jiǎn)單。
我不認(rèn)為這樣一篇博客就能 Kodein從入門到精通辛燥,所謂實(shí)踐出真知筛武,我更建議您參考實(shí)際的項(xiàng)目,去了解它在實(shí)際項(xiàng)目中的應(yīng)用:
https://github.com/qingmei2/MVVM-Rhine
最后列一下相關(guān)學(xué)習(xí)資料(筆者寫稿的此時(shí)挎塌,國(guó)內(nèi)尚未有任何Kodein的中文學(xué)習(xí)資料徘六,實(shí)在遺憾),以供大家參考:
Kodein官方文檔:Getting started with Kodein DI
Kodein官方文檔 for Android:Kodein DI on Android
2018/11/30補(bǔ)充
感謝 o動(dòng)感超人o 兄弟花了很久總結(jié)的圖榴都,將Kodein官方文檔晦澀的文字總結(jié)了出來待锈,經(jīng)過他的同意,我把這張圖也轉(zhuǎn)載過來嘴高,以供大家參考:
原文鏈接:《使用Kodein作為Dagger2的升級(jí)版替代品》 by o動(dòng)感超人o
原圖鏈接:http://naotu.baidu.com/file/7f5ea2ba8e7fa2820973d11b7c66a98a
--------------------------廣告分割線------------------------------
關(guān)于我
Hello竿音,我是卻把清梅嗅和屎,如果您覺得文章對(duì)您有價(jià)值,歡迎 ??春瞬,也歡迎關(guān)注我的博客或者Github柴信。
如果您覺得文章還差了那么點(diǎn)東西,也請(qǐng)通過關(guān)注督促我寫出更好的文章——萬(wàn)一哪天我進(jìn)步了呢宽气?