前言
之前接手的一個項目里有些代碼看得云里霧里的噩凹,找了半天沒有找到對象創(chuàng)建的地方,后來才發(fā)現(xiàn)原來使用了Hilt進(jìn)行了依賴注入毡咏。Hilt相比Dagger雖然已經(jīng)比較簡潔驮宴,但對初學(xué)者來說還是有些門檻,并且網(wǎng)上的許多文章都是搬自官網(wǎng)呕缭,入手容易深入難堵泽,如果你對Hilt不了解或是想了解得更多,那么接下來的內(nèi)容將助力你玩轉(zhuǎn)Hilt臊旭。
通過本篇文章落恼,你將了解到:
- 什么是依賴注入?
- Hilt 的引入與基本使用
- Hilt 的進(jìn)階使用
- Hilt 原理簡單分析
- Android到底該不該使用DI框架?
1. 什么是依賴注入离熏?
什么是依賴佳谦?
以手機(jī)為例,要組裝一臺手機(jī)滋戳,我們需要哪些部件呢钻蔑?
從宏觀上分類:軟件+硬件。
由此我們可以說:手機(jī)依賴了軟件和硬件奸鸯。
而反映到代碼的世界:
class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打電話
software.handle()
hardware.handle()
}
}
//軟件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}
FishPhone 依賴了兩個對象:分別是Software和Hardware咪笑。
Software和Hardware是FishPhone的依賴(項)。
什么是注入娄涩?
上面的Demo窗怒,F(xiàn)ishPhone內(nèi)部自主構(gòu)造了依賴項的實(shí)例映跟,考慮到依賴的變化挺大的,每次依賴項的改變都要改動到FishPhone扬虚,容易出錯努隙,也不是那么靈活,因此考慮從外部將依賴傳進(jìn)來辜昵,這種方式稱之為:依賴注入(Dependency Injection 簡稱DI)
有幾種方式:
- 構(gòu)造函數(shù)傳入
- SetXX函數(shù)傳入
- 從其它對象間接獲取
構(gòu)造函數(shù)依賴注入:
class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打電話
software.handle()
hardware.handle()
}
}
FishPhone的功能比較純粹就是打電話功能荸镊,而依賴項都是外部傳入提升了靈活性。
為什么需要依賴注入框架堪置?
手機(jī)制造出來后交給客戶使用躬存。
class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}
用戶想使用手機(jī)打電話,還得自己創(chuàng)建軟件和硬件舀锨,這個手機(jī)還能賣出去嗎岭洲?
而不想創(chuàng)建軟件和硬件那得讓FishPhone自己負(fù)責(zé)去創(chuàng)建,那不是又回到上面的場景了嗎雁竞?
你可能會說:FishPhone內(nèi)部就依賴了兩個對象而已钦椭,自己負(fù)責(zé)創(chuàng)建又怎么了?
解耦
再看看如下Demo:
interface ISoftware {
fun handle()
}
//硬件
interface IHardware {
fun handle()
}
//軟件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}
//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}
class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打電話
software.handle()
hardware.handle()
}
}
FishPhone 只關(guān)注軟件和硬件的接口碑诉,至于具體怎么實(shí)現(xiàn)它不關(guān)心彪腔,這就達(dá)到了解耦的目的。
既然要解耦进栽,那么SoftwareImpl()德挣、HardwareImpl()就不能出現(xiàn)在FishPhone里。
應(yīng)該改為如下形式:
class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打電話
software.handle()
hardware.handle()
}
}
消除模板代碼
即使我們不考慮解耦快毛,假若HardwareImpl里又依賴了cpu格嗅、gpu、disk等模塊:
//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模塊
override fun handle() {}
}
現(xiàn)在僅僅只是三個模塊唠帝,若是依賴更多的模塊或者模塊的本身也需要依賴其它子模塊屯掖,比如CPU需要依賴寄存器、運(yùn)算單元等等襟衰,那么我們就需要寫更多的模板代碼贴铜,要是我們只需要聲明一下想要使用的對象而不用管它的創(chuàng)建就好了。
class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}
可以看出瀑晒,下面的代碼比上面的簡潔多了绍坝。
- 從解耦和消除模板代碼的角度看,我們迫切需要一個能夠自動創(chuàng)建依賴對象并且將依賴注入到目標(biāo)代碼的框架苔悦,這就是依賴注入框架
- 依賴注入框架能夠管理依賴對象的創(chuàng)建轩褐,依賴對象的注入,依賴對象的生命周期
- 使用者僅僅只需要表明自己需要什么類型的對象玖详,剩下的無需關(guān)心把介,都由框架自動完成
先想想若是我們想要實(shí)現(xiàn)這樣的框架需要怎么做呢勤讽?
相信很多小伙伴最樸素的想法就是:使用工廠模式,你傳參告訴我想要什么對象我給你構(gòu)造出來拗踢。
這個想法是半自動注入地技,因為我們還要調(diào)用工廠方法去獲取,而全自動的注入通常來說是使用注解標(biāo)注實(shí)現(xiàn)的秒拔。
2. Hilt 的引入與基本使用
Hilt的引入
從Dagger到Dagger2再到Hilt(Android專用),配置越來越簡單也比較容易上手飒硅。
前面說了依賴注入框架的必要性砂缩,我們就想迫不及待的上手,但難度可想而知三娩,還好大神們早就造好了輪子庵芭。
以AGP 7.0 以上為例,來看看Hilt框架是如何引入的雀监。
一:project級別的build.gradle 引入如下代碼:
plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}
二:module級別的build.gradle引入如下代碼:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代碼
id 'kotlin-kapt'
}
//引入庫
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'
實(shí)時更新最新版本以及AGP7.0以下的引用請參考:Hilt最新版本配置
Hilt的簡單使用
前置步驟整好了接下來看看如何使用双吆。
一:表明該App可以使用Hilt來進(jìn)行依賴注入,添加如下代碼:
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}
@HiltAndroidApp 添加到App的入口会前,即表示依賴注入的環(huán)境已經(jīng)搭建好好乐。
二:注入一個對象到MyApp里:
有個類定義如下:
class Software {
val name = "fish"
}
我們不想顯示的構(gòu)造它,想借助Hilt注入它瓦宜,那得先告訴Hilt這個類你幫我注入一下蔚万,改為如下代碼:
class Software @Inject constructor() {
val name = "fish"
}
在構(gòu)造函數(shù)前添加了@Inject注解,表示該類可以被注入临庇。
而在MyApp里使用Software對象:
@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software
override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}
對引用的對象使用@Inject注解反璃,表示期望Hilt幫我將這個對象new出來。
最后查看打印輸出正確假夺,說明Software對象被創(chuàng)建了淮蜈。
這是最簡單的Hilt應(yīng)用,可以看出:
- 我們并沒有顯式地創(chuàng)建Software對象已卷,而Hilt在適當(dāng)?shù)臅r候就幫我們創(chuàng)建好了
- @HiltAndroidApp 只用于修飾Application
如何注入接口梧田?
一:錯誤示范
上面提到過,使用DI的好處之一就是解耦悼尾,而我們上面注入的是類柿扣,現(xiàn)在我們將Software抽象為接口,很容易就會想到如下寫法:
interface ISoftware {
fun printName()
}
class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}
@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware
override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}
不幸的是上述代碼編譯失敗闺魏,Hilt提示說不能對接口使用注解未状,因為我們并沒有告訴Hilt是誰實(shí)現(xiàn)了ISoftware,而接口本身不能直接實(shí)例化析桥,因此我們需要為它指定具體的實(shí)現(xiàn)類司草。
二:正確示范
再定義一個類如下:
@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}
- @Module 表示該類是一個Hilt的Module艰垂,固定寫法
- @InstallIn 表示模塊在哪個組件生命周期內(nèi)生效,SingletonComponent::class指的是全局
- 一個抽象類埋虹,類名隨意
- 抽象方法猜憎,方法名隨意,返回值是需要被注入的對象類型(接口)搔课,而參數(shù)是該接口的實(shí)現(xiàn)類胰柑,使用@Binds注解標(biāo)記,
如此一來我們就告訴了Hilt爬泥,SoftwareImpl是ISoftware的實(shí)現(xiàn)類柬讨,于是Hilt注入ISoftware對象的時候就知道使用SoftwareImpl進(jìn)行實(shí)例化。
其它不變運(yùn)行一下:
可以看出袍啡,實(shí)際注入的是SoftwareImpl踩官。
@Binds 適用在我們能夠修改類的構(gòu)造函數(shù)的場景
如何注入第三方類
上面的SoftwareImpl是我們可以修改的,因為使用了@Inject修飾其構(gòu)造函數(shù)境输,所以可以在其它地方注入它蔗牡。
在一些時候我們不想使用@Inject修飾或者說這個類我們不能修改,那該如何注入它們呢嗅剖?
一:定義Provides模塊
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}
- @Module和@InstallIn 注解是必須的
- 定義object類
- 定義函數(shù)辩越,方法名隨意,返回類型為我們需要注入的類型
- 函數(shù)體里通過構(gòu)造或是其它方式創(chuàng)建具體實(shí)例
- 使用@Provides注解函數(shù)
二:依賴使用
而Hardware定義如下:
class Hardware {
fun printName() {
println("I'm fish")
}
}
在MyApp里引用Hardware:
雖然Hardware構(gòu)造函數(shù)沒有使用@Inject注解信粮,但是我們依然能夠使用依賴注入区匣。
當(dāng)然我們也可以注入接口:
interface IHardware {
fun printName()
}
class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}
想要注入IHardware接口,需要定義provides模塊:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}
@Provides適用于無法修改類的構(gòu)造函數(shù)的場景蒋院,多用于注入第三方的對象
3. Hilt 的進(jìn)階使用
限定符
上述 ISoftware的實(shí)現(xiàn)類只有一個亏钩,假設(shè)現(xiàn)在有兩個實(shí)現(xiàn)類呢?
比如說這些軟件可以是美國提供欺旧,也可以是中國提供的姑丑,依據(jù)上面的經(jīng)驗我們很容易寫出如下代碼:
class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}
class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware
@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}
//依賴注入:
@Inject
lateinit var software: ISoftware
興高采烈的進(jìn)行編譯,然而卻報錯:
也就是說Hilt想要注入ISoftware辞友,但不知道選擇哪個實(shí)現(xiàn)類栅哀,SoftwareChina還是SoftwareUS?沒人告訴它称龙,所以它迷茫了留拾,索性都綁定了。
這個時候我們需要借助注解:@Qualifier 限定符注解來對實(shí)現(xiàn)類進(jìn)行限制鲫尊。
改造一下:
@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware
@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China
定義新的注解類痴柔,使用@Qualifier修飾。
而后在Module里疫向,分別使用注解類修飾返回的函數(shù)咳蔚,如bindSoftwareCh函數(shù)指定返回SoftwareChina來實(shí)現(xiàn)ISoftware接口豪嚎。
最后在引用依賴注入的地方分別使用@China @US修飾。
@Inject
@US
lateinit var software1: ISoftware
@Inject
@China
lateinit var software2: ISoftware
此時谈火,雖然software1侈询、software2都是ISoftware類型,但是由于我們指定了限定符@US糯耍、@China扔字,因此最后真正的實(shí)現(xiàn)類分別是SoftwareChina、SoftwareUS温技。
@Qualifier 主要用在接口有多個實(shí)現(xiàn)類(抽象類有多個子類)的注入場景
預(yù)定義限定符
上面提及的限定符我們還可以擴(kuò)展其使用方式啦租。
你可能發(fā)現(xiàn)了,上述提及的可注入的類構(gòu)造函數(shù)都是無參的荒揣,很多時候我們的構(gòu)造函數(shù)是需要有參數(shù)的,比如:
class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software
這個時候編譯會報錯:
意思是Software依賴的Context沒有進(jìn)行注入焊刹,因此我們需要給它注入一個Context系任。
由上面的分析可知,Context類不是我們可以修改的虐块,只能通過@Provides方式提供其注入實(shí)例俩滥,并且Context有很多子類,我們需要使用@Qualifier指定具體實(shí)現(xiàn)類贺奠,因此很容易我們就想到如下對策霜旧。
先定義Module:
@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext
再注入Context:
class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
可以看出,借助@Provides和@Qualifier儡率,可以實(shí)現(xiàn)全局的Context挂据。
當(dāng)然了,實(shí)際上我們無需如此麻煩儿普,因為這部分工作Hilt已經(jīng)預(yù)先幫我們弄了崎逃。
與我們提供的限定符注解GlobalContext類似,Hilt預(yù)先提供了:
@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}
因此我們只需要在需要的地方引用它即可:
class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
如此一來我們無需重新定義Module眉孩。
- 除了提供Application級別的上下文:@ApplicationContext个绍,Hilt還提供了Activity級別的上下文:@ActivityContext,因為是Hilt內(nèi)置的限定符浪汪,因此稱為預(yù)定義限定符巴柿。
- 如果想自己提供限定符,可以參照GlobalContext的做法死遭。
組件作用域和生命周期
Hilt支持的注入點(diǎn)(類)
以上的demo都是在MyApp里進(jìn)行依賴广恢,MyApp里使用了注解:@HiltAndroidApp 修飾,表示當(dāng)前App支持Hilt依賴呀潭,Application就是它支持的一個注入點(diǎn)袁波,現(xiàn)在想要在Activity里使用Hilt呢瓦阐?
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
除了Application和Activity,Hilt內(nèi)置支持的注入點(diǎn)如下:
除了Application和ViewModel篷牌,其它注入點(diǎn)都是通過使用@AndroidEntryPoint修飾睡蟋。
注入點(diǎn)其實(shí)就是依賴注入開始的點(diǎn),比如Activity里需要注入A依賴枷颊,A里又需要注入B依賴戳杀,B里又需要注入C依賴,從Activity開始我們就能構(gòu)建所有的依賴
Hilt組件的生命周期
什么是組件夭苗?在Dagger時代我們需要自己寫組件信卡,而在Hilt里組件都是自動生成的,無需我們干預(yù)题造。
依賴注入的本質(zhì)實(shí)際上就是在某個地方悄咪咪地創(chuàng)建對象傍菇,這個地方的就是組件,Hilt專為Android打造界赔,因此勢必適配了Android的特性丢习,比如生命周期這個Android里的重中之重。
因此Hilt的組件有兩個主要功能:
- 創(chuàng)建淮悼、注入依賴的對象
- 管理對象的生命周期
Hilt組件如下:
可以看出咐低,這些組件的創(chuàng)建和銷毀深度綁定了Android常見的生命周期。
你可能會說:上面貌似沒用到組件相關(guān)的東西袜腥,看了這么久也沒看懂啊见擦。
繼續(xù)看個例子:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}
@InstallIn(SingletonComponent::class) 表示把模塊安裝到SingletonComponent組件里,SingletonComponent組件顧名思義是全局的羹令,對應(yīng)的是Application級別鲤屡。因此安裝的這個模塊可在整個App里使用。
問題來了:SingletonComponent是不是表示@Provides修飾的函數(shù)返回的實(shí)例是同一個福侈?
答案是否定的执俩。
這就涉及到組件的作用域。
組件的作用域
想要上一小結(jié)的代碼提供全局唯一實(shí)例癌刽,則可用組件作用域注解修飾函數(shù):
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}
當(dāng)我們在任何地方注入IHardware時役首,獲取到的都是同一個實(shí)例。
除了@Singleton表示組件的作用域显拜,還有其它對應(yīng)組件的作用域:
簡單解釋作用域:
@Singleton 被它修飾的構(gòu)造函數(shù)或是函數(shù)衡奥,返回的始終是同一個實(shí)例
@ActivityRetainedScoped 被它修飾的構(gòu)造函數(shù)或是函數(shù),在Activity的重建前后返回同一實(shí)例
@ActivityScoped 被它修飾的構(gòu)造函數(shù)或是函數(shù)远荠,在同一個Activity對象里矮固,返回的都是同一實(shí)例
@ViewModelScoped 被它修飾的構(gòu)造函數(shù)或是函數(shù),與ViewModel規(guī)則一致
- Hilt默認(rèn)不綁定任何作用域,由此帶來的結(jié)果是每一次注入都是全新的對象
- 組件的作用域要么不指定档址,要指定那必須和組件的生命周期一致
以下幾種寫法都不符合第二種限制:
@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//錯誤盹兢,和組件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}
@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//錯誤,和組件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}
@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//錯誤守伸,和組件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}
除了修飾Module绎秒,作用域還可以用于修飾構(gòu)造函數(shù):
@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}
@ActivityScoped表示不管注入幾個Hardware,在同一個Activity里注入的實(shí)例都是一致的尼摹。
構(gòu)造函數(shù)里無法注入的字段
一個類的構(gòu)造函數(shù)如果被@Inject注入见芹,那么構(gòu)造函數(shù)的其它參數(shù)都需要支持注入。
class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}
以上代碼是無法編譯通過的蠢涝,因為Context不支持注入玄呛,而通過上面的分析可知,我們可以使用限定符:
class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}
這就可以成功注入了和二。
再看看此種場景:
class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}
很顯然String不支持注入徘铝,當(dāng)然我們可以向@ApplicationContext 一樣也給String提供一個@Provides和@Qualifier注解,但可想而知很麻煩惯吕,關(guān)鍵是String是動態(tài)變化的惕它,我們確實(shí)需要Hardware構(gòu)造的時候傳入合適的String。
由此引入新的寫法:輔助注入
class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {
//輔助工廠類
@AssistedFactory
interface Factory{
//不支持注入的參數(shù)都可以放這混埠,返回值為待注入的類型
fun create(version: String):Hardware
}
fun printName() {
println("I'm fish")
}
}
在引用注入的地方不能直接使用Hardware,而是需要通過輔助工廠進(jìn)行創(chuàng)建:
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)
val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}
如此一來诗轻,通過輔助注入钳宪,我們還是可以使用Hilt,值得一提的是輔助注入不是Hilt獨(dú)有扳炬,而是從Dagger繼承來的功能吏颖。
自定義注入點(diǎn)
Hilt僅僅內(nèi)置了常用的注入點(diǎn):Application、Activity恨樟、Fragment半醉、ViewModel等。
思考一種場景:小明同學(xué)寫的模塊都是需要注入:
class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}
class GPU @Inject constructor(val videoStorage: VideoStorage){}
//顯存
class VideoStorage @Inject constructor() {}
class CPU @Inject constructor(val register: Register) {}
//寄存器
class Register @Inject() constructor() {}
此時小剛需要引用Hardware劝术,他有兩種選擇:
- 使用注入方式很容易就引用了Hardware缩多,可惜的是他沒有注入點(diǎn),僅僅只是工具類养晋。
- 不選注入方式衬吆,則需要構(gòu)造Hardware實(shí)例,而Hardware依賴GPU和CPU绳泉,它們又分別依賴VideoStorage和Register逊抡,想要成功構(gòu)造Hardware實(shí)例需要將其它的依賴實(shí)例都手動構(gòu)造出來,可想而知很麻煩零酪。
這個時候適合小剛的方案是:
自定義注入點(diǎn)
方案實(shí)施步驟:
一:定義入口點(diǎn)
@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//該注入點(diǎn)負(fù)責(zé)返回Hardware實(shí)例
fun getHardware(): Hardware
}
二:通過入口點(diǎn)獲取實(shí)例
class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}
三:使用Hardware
val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")
注入object類
定義了object類冒嫡,但在注入的時候也需要拇勃,可以做如下處理:
object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}
@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}
4. Hilt 原理簡單分析
@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}
Hilt通過apt在編譯時期生成代碼:
public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
private boolean injected = false;
Hilt_SecondActivity() {
super();
//初始化注入監(jiān)聽
_initHiltInternal();
}
Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}
private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}
protected void inject() {
if (!injected) {
injected = true;
//通過manager獲取組件,再通過組件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}
在編譯期孝凌,SecondActivity的父類由AppCompatActivity變?yōu)镠ilt_SecondActivity方咆,因此當(dāng)SecondActivity構(gòu)造時就會調(diào)用父類的構(gòu)造器監(jiān)聽create()的回調(diào),回調(diào)調(diào)用時進(jìn)行注入胎许。
由此可見峻呛,Activity.onCreate()執(zhí)行后,Hilt依賴注入的字段才會有值
真正注入的過程涉及到不少的類辜窑,都是自動生成的類钩述,有興趣可以對著源碼查找流程,此處就不展開說了穆碎。
5. Android到底該不該使用DI框架?
有人說DI比較復(fù)雜牙勘,還不如我直接構(gòu)造呢?
又有人說那是你項目不復(fù)雜所禀,用不到方面,在后端流行的Spring全家桶,依賴注入大行其道色徘,Android復(fù)雜的項目也需要DI來解耦恭金。
從個人的實(shí)踐經(jīng)驗看,Android MVVM/MVI 模式還是比較適合引入Hilt的褂策。
摘抄官網(wǎng)的:現(xiàn)代Android 應(yīng)用架構(gòu)
通常來說我們這么設(shè)計UI層到數(shù)據(jù)層的架構(gòu):
class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}
class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}
//遠(yuǎn)程來源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}
class MyRetrofit @Inject constructor(
) {}
//本地來源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}
class MyDataStore @Inject constructor() {}
可以看出横腿,層次比較深,使用了Hilt簡潔了許多斤寂。
本文基于 Hilt 2.48.1
參考文檔:
https://dagger.dev/hilt/gradle-setup
https://developer.android.com/topic/architecture/recommendations?hl=zh-cn
https://repo.maven.apache.org/maven2/com/google/dagger/hilt/android/com.google.dagger.hilt.android.gradle.plugin/