Android使用Hilt依賴注入,讓人看不懂你代碼

前言

之前接手的一個項目里有些代碼看得云里霧里的噩凹,找了半天沒有找到對象創(chuàng)建的地方,后來才發(fā)現(xiàn)原來使用了Hilt進(jìn)行了依賴注入毡咏。Hilt相比Dagger雖然已經(jīng)比較簡潔驮宴,但對初學(xué)者來說還是有些門檻,并且網(wǎng)上的許多文章都是搬自官網(wǎng)呕缭,入手容易深入難堵泽,如果你對Hilt不了解或是想了解得更多,那么接下來的內(nèi)容將助力你玩轉(zhuǎn)Hilt臊旭。

通過本篇文章落恼,你將了解到:

  1. 什么是依賴注入?
  2. Hilt 的引入與基本使用
  3. Hilt 的進(jìn)階使用
  4. Hilt 原理簡單分析
  5. 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)
有幾種方式:

  1. 構(gòu)造函數(shù)傳入
  2. SetXX函數(shù)傳入
  3. 從其它對象間接獲取

構(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() {}
}

可以看出瀑晒,下面的代碼比上面的簡潔多了绍坝。

  1. 從解耦和消除模板代碼的角度看,我們迫切需要一個能夠自動創(chuàng)建依賴對象并且將依賴注入到目標(biāo)代碼的框架苔悦,這就是依賴注入框架
  2. 依賴注入框架能夠管理依賴對象的創(chuàng)建轩褐,依賴對象的注入,依賴對象的生命周期
  3. 使用者僅僅只需要表明自己需要什么類型的對象玖详,剩下的無需關(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)用,可以看出:

  1. 我們并沒有顯式地創(chuàng)建Software對象已卷,而Hilt在適當(dāng)?shù)臅r候就幫我們創(chuàng)建好了
  2. @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
}
  1. @Module 表示該類是一個Hilt的Module艰垂,固定寫法
  2. @InstallIn 表示模塊在哪個組件生命周期內(nèi)生效,SingletonComponent::class指的是全局
  3. 一個抽象類埋虹,類名隨意
  4. 抽象方法猜憎,方法名隨意,返回值是需要被注入的對象類型(接口)搔课,而參數(shù)是該接口的實(shí)現(xiàn)類胰柑,使用@Binds注解標(biāo)記,

如此一來我們就告訴了Hilt爬泥,SoftwareImpl是ISoftware的實(shí)現(xiàn)類柬讨,于是Hilt注入ISoftware對象的時候就知道使用SoftwareImpl進(jìn)行實(shí)例化。
其它不變運(yùn)行一下:


image.png

可以看出袍啡,實(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()
    }
}
  1. @Module和@InstallIn 注解是必須的
  2. 定義object類
  3. 定義函數(shù)辩越,方法名隨意,返回類型為我們需要注入的類型
  4. 函數(shù)體里通過構(gòu)造或是其它方式創(chuàng)建具體實(shí)例
  5. 使用@Provides注解函數(shù)

二:依賴使用
而Hardware定義如下:

class Hardware {
    fun printName() {
        println("I'm fish")
    }
}

在MyApp里引用Hardware:


image.png

雖然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)行編譯,然而卻報錯:


image.png

也就是說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

這個時候編譯會報錯:


image.png

意思是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眉孩。

  1. 除了提供Application級別的上下文:@ApplicationContext个绍,Hilt還提供了Activity級別的上下文:@ActivityContext,因為是Hilt內(nèi)置的限定符浪汪,因此稱為預(yù)定義限定符巴柿。
  2. 如果想自己提供限定符,可以參照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)如下:


image.png

除了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的組件有兩個主要功能:

  1. 創(chuàng)建淮悼、注入依賴的對象
  2. 管理對象的生命周期

Hilt組件如下:


image.png

可以看出咐低,這些組件的創(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)組件的作用域:


image.png

簡單解釋作用域:
@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ī)則一致

  1. Hilt默認(rèn)不綁定任何作用域,由此帶來的結(jié)果是每一次注入都是全新的對象
  2. 組件的作用域要么不指定档址,要指定那必須和組件的生命周期一致

以下幾種寫法都不符合第二種限制:

@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劝术,他有兩種選擇:

  1. 使用注入方式很容易就引用了Hardware缩多,可惜的是他沒有注入點(diǎn),僅僅只是工具類养晋。
  2. 不選注入方式衬吆,則需要構(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的褂策。


image.png

摘抄官網(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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耿焊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子遍搞,更是在濱河造成了極大的恐慌罗侯,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溪猿,死亡現(xiàn)場離奇詭異钩杰,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)诊县,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門榜苫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翎冲,你說我怎么就攤上這事垂睬。” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵驹饺,是天一觀的道長钳枕。 經(jīng)常有香客問我,道長赏壹,這世上最難降的妖魔是什么鱼炒? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蝌借,結(jié)果婚禮上昔瞧,老公的妹妹穿的比我還像新娘。我一直安慰自己菩佑,他們只是感情好自晰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著稍坯,像睡著了一般酬荞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瞧哟,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天混巧,我揣著相機(jī)與錄音,去河邊找鬼勤揩。 笑死咧党,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陨亡。 我是一名探鬼主播傍衡,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼数苫!你這毒婦竟也來了聪舒?” 一聲冷哼從身側(cè)響起辨液,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤虐急,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后滔迈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體止吁,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡古掏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年拦赠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辅愿。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡谈山,死狀恐怖俄删,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤畴椰,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布臊诊,位于F島的核電站,受9級特大地震影響斜脂,放射性物質(zhì)發(fā)生泄漏抓艳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一帚戳、第九天 我趴在偏房一處隱蔽的房頂上張望玷或。 院中可真熱鬧,春花似錦片任、人聲如沸偏友。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽约谈。三九已至,卻和暖如春犁钟,著一層夾襖步出監(jiān)牢的瞬間棱诱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工涝动, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迈勋,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓醋粟,卻偏偏與公主長得像靡菇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子米愿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容