Android日志:Hilt

引入

在了解Hilt之前,我們先來了解一個(gè)重要的概念——依賴注入(以下來源:Android開發(fā)者文檔

依賴注入(DI)是一種廣泛用于編程的技術(shù),非常適用于Android開發(fā)。遵循DI的原則可以良好的應(yīng)用架構(gòu)奠定基礎(chǔ)。
實(shí)現(xiàn)依賴注入可以帶來以下優(yōu)勢(shì):1、重用代碼;2、易于重構(gòu);3、易于測(cè)試

什么是依賴項(xiàng)注入

類通常需要引用其他類。例如Car類可能需要引用Engine類。這些必需類成為依賴項(xiàng),在此實(shí)例中,Car類依賴于擁有Engine類的實(shí)例才能實(shí)現(xiàn)。

class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

類通常有三種方式獲取所需的對(duì)象:
1、類構(gòu)造其所需的依賴項(xiàng)。在以上示例中,Car將創(chuàng)建并初始化自己的Engine實(shí)例。
2、從其他地方抓取。某些Android API(如getSystemService())的工作原理就是如此。
3、以參數(shù)形式提供。應(yīng)用可以在構(gòu)造類時(shí)提供這些依賴項(xiàng),或者將這些依賴項(xiàng)傳入需要各個(gè)依賴項(xiàng)的函數(shù)。在以上示例中,Car構(gòu)造函數(shù)將接收Engine作為參數(shù)。這就是所謂的依賴項(xiàng)注入,使用這種方法,我們可以獲取并提供類的依賴項(xiàng),而不必讓類實(shí)例自行獲取。

class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

使用這種方法的好處顯而易見:作為Car 的構(gòu)造函數(shù)參數(shù),可以輕松地進(jìn)行拆卸并測(cè)試其他類(Engine的子類)。
Android中有兩種主要的手動(dòng)依賴項(xiàng)注入方式:

  • 構(gòu)造函數(shù)注入,也就是上方提及的方式
  • 字段注入(或setter注入)。某些Android框架類(如Activity和Fragment)由系統(tǒng)實(shí)例化,因此無法進(jìn)行構(gòu)造函數(shù)注入。使用字段注入時(shí),依賴項(xiàng)將在創(chuàng)建類后實(shí)例化。
    說的那么高端,其實(shí)就是延遲加載
class Car {
    lateinit var engine: Engine

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.engine = Engine()
    car.start()
}

注:依賴想注入基于控制反轉(zhuǎn)原則,根據(jù)該原則,通用代碼控制特定的執(zhí)行。

依賴性注入的替代方法

依賴項(xiàng)注入的替代方法是使用服務(wù)定位器(或者我們一般更喜歡叫工具類)。服務(wù)定位器設(shè)計(jì)模式還改進(jìn)了類與具體依賴項(xiàng)的分離??梢詣?chuàng)建一個(gè)名為服務(wù)定位器的類,該類創(chuàng)建和存儲(chǔ)依賴項(xiàng),然后按需提供這些依賴項(xiàng)。

object ServiceLocator {
    fun getEngine(): Engine = Engine()
}

class Car {
    private val engine = ServiceLocator.getEngine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

服務(wù)定位器模式與依賴項(xiàng)注入在元素使用方式上有所不同。使用服務(wù)定位器模式,類可以控制并請(qǐng)求注入對(duì)象;使用依賴想注入,應(yīng)用可以控制并主動(dòng)注入所需對(duì)象。


手動(dòng)實(shí)現(xiàn)依賴項(xiàng)注入的實(shí)戰(zhàn)案例

這是Android開發(fā)平臺(tái)提供的一個(gè)引入Hilt的一個(gè)例子,里面從最初的依賴注入講起,逐步規(guī)范依賴注入模式,最后達(dá)到和Dagger相接近的案例。
https://developer.android.google.cn/training/dependency-injection/manual?hl=zh_cn
其中涉及到MVVM涉及模式架構(gòu)的搭建,在我往期的實(shí)戰(zhàn)項(xiàng)目中也有體現(xiàn)。

總結(jié)
依賴項(xiàng)注入對(duì)于創(chuàng)建可擴(kuò)展且可測(cè)試的Android應(yīng)用而言是一項(xiàng)適合的技術(shù)。將容器作為在應(yīng)用的不同部分共享各個(gè)類實(shí)例的一種方式,以及使用工廠類創(chuàng)建各個(gè)類實(shí)例的集中位置。
當(dāng)應(yīng)用變大時(shí),我們會(huì)發(fā)現(xiàn)我們的項(xiàng)目編寫了大量樣板代碼(如工廠類),這可能容易出錯(cuò)。我們還必須自行管理容器的范圍和生命周期,優(yōu)化并舍棄不再需要的容器以釋放內(nèi)存。如果操作不當(dāng),可能會(huì)呆滯應(yīng)用出現(xiàn)微小錯(cuò)誤和內(nèi)存泄漏。


使用Hilt實(shí)現(xiàn)依賴注入

Hilt是Android的依賴項(xiàng)注入庫,可減少在項(xiàng)目中執(zhí)行手動(dòng)依賴項(xiàng)注入的樣板代碼。執(zhí)行手動(dòng)依賴項(xiàng)注入要求我們手動(dòng)構(gòu)造每個(gè)類機(jī)器依賴項(xiàng),并借助容器重復(fù)使用和管理依賴項(xiàng)。
Hilt通過為項(xiàng)目中的每個(gè)Android類提供容器并自動(dòng)管理其生命周期,提供了一種在應(yīng)用中使用DI(依賴項(xiàng)注入)的標(biāo)準(zhǔn)方法。Hilt是在Dagger的基礎(chǔ)上構(gòu)建而成的。

添加依賴

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

如果這里在配置的時(shí)候報(bào)錯(cuò),將單引號(hào)更改為雙引號(hào)即可。

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

在Application中添加@HiltAndroidApp注解

所有使用Hilt的應(yīng)用都必須包含一個(gè)帶有@HiltAndroidApp注解的Application類。

@HiltAndroidApp
class MyApplication:Application() {
    
}

為什么需要一個(gè)Application并且需要添加注解@HiltAndroidApp?
@HiltAndroidApp會(huì)觸發(fā)Hilt的代碼生成操作,生成的代碼包括應(yīng)用的一個(gè)基類,該基類充當(dāng)應(yīng)用級(jí)依賴項(xiàng)容器。
生成的這一Hilt組件會(huì)附加到Application對(duì)象的聲明周期,并為其提供依賴項(xiàng)。此外,它也是應(yīng)用的父組件,這意味著,其他組件可以訪問它提供的依賴項(xiàng)。

將依賴注入Android類

在Application類中設(shè)置了Hilt且有了應(yīng)用及組建后,Hilt可以為帶有@AndroidEntryPoint的其他Android類提供依賴項(xiàng)

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

如果使用@AndroidEntryPoint為某個(gè)Android類添加注解,則必須為依賴于該類的Android類添加注解。例如,如果我們?yōu)槟硞€(gè)Fragment添加注解,則必須為使用該Fragment的所有Activity添加注解。
注:由Hilt注入的字段不能為私有字段。會(huì)導(dǎo)致編譯報(bào)錯(cuò)
@AndroidEntryPoint會(huì)為項(xiàng)目中的每個(gè)Android類生成一個(gè)單獨(dú)的Hilt組件。這些組件可以從它們各自的父類接收依賴項(xiàng)。
如需從組件獲取依賴項(xiàng),需使用@inject注解執(zhí)行字段注入:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapte
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

Hilt注入的類可以有同樣使用注入的其他基類。如果這些類是抽象類,則它們不需要@AndroidEntryPoint。


定義Hilt綁定

/**
 * 為了執(zhí)行字段注入,Hilt需要知道如何從相應(yīng)組件提供必要依賴項(xiàng)的實(shí)例 ->綁定
 * 綁定包含將某個(gè)類型的實(shí)例作為依賴項(xiàng)提供所需的信息。
 * 向Hilt提供綁定信息的方法是構(gòu)造函數(shù)注入。在某個(gè)類的構(gòu)造函數(shù)中使用@Inject注解,
 * 以告知Hilt如何提供該類的實(shí)例
 *
 * Student是MyAdapter的一個(gè)依賴項(xiàng),所以Hilt必須知道如何提供Student的實(shí)例
 */
class MyAdapter @Inject constructor(private val student: Student) {
}

Hilt模塊

有時(shí),類型不能通過構(gòu)造函數(shù)注入。發(fā)生這種情況的原因有很多。例如,您不能通過構(gòu)造函數(shù)注入接口。此外,您也不能通過構(gòu)造函數(shù)注入不歸您所有的類型,如來自外部庫的類。在這些情況下,您可以通過使用Hilt模塊向Hilt提供綁定信息。
Hilt模塊是一個(gè)帶有@Module注解的類。 它會(huì)告知Hilt如何提供某些類型的實(shí)例。還必須使用@InstallIn為Hilt模塊添加注解,以告知Hilt每個(gè)模塊將用在或安裝在哪個(gè)Android類中。
在Hilt模塊中提供的依賴項(xiàng)可以在生成的所有與Hilt模塊安裝到的Android類關(guān)聯(lián)的組件中使用。
注:由于Hilt的代碼生成操作需要訪問使用Hilt 的所有Gradle模塊,因此編譯Application類的Gradle模塊還需要在其傳遞依賴項(xiàng)中包含您的所有Hilt模塊和通過構(gòu)造函數(shù)注入的類。

/**
 *@Description
 *@Author PC
 *@QQ 1578684787
 */

/**
 * 使用@Binds注入接口實(shí)例
 * 接口是無法通過構(gòu)造函數(shù)注入的,而應(yīng)該向Hilt提供綁定信息
 * -> 在Hilt模塊內(nèi)創(chuàng)建一個(gè)帶有@Binds注解的抽象函數(shù)
 * @Binds 注解會(huì)告知Hilt在需要提供接口的實(shí)例時(shí)要使用哪種實(shí)現(xiàn)
 * 帶有注解的函數(shù)回想Hilt提供以下信息:
 *    - 函數(shù)返回類型會(huì)告知Hilt函數(shù)提供哪個(gè)接口的實(shí)例。
 *    - 函數(shù)參數(shù)會(huì)告知Hilt要提供哪種實(shí)現(xiàn)。
 */

//1、需要一個(gè)接口
interface AnalyticService{
    fun analyticsMethods()
}


//2、接口實(shí)現(xiàn)
//Hilt也需要知道如何提供AnalyticsServiceImpl的實(shí)例
class AnalyticsServiceImpl @Inject constructor():AnalyticService {
    override fun analyticsMethods() {

    }

}

//@InstallIn(ActivityComponent::class) 意味著所有依賴項(xiàng)都可以在應(yīng)用的Activity中使用,
// 同理還有ApplicationComponent
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsNodule{
    
    
    //3、注入接口實(shí)現(xiàn)
    //提供接口類型的返回值,體現(xiàn)多態(tài)的作用,返回值可以切換任意繼承該接口的類的參數(shù)
    @Binds
    abstract fun bindAnalyticsService(
            analyticsServiceImpl: AnalyticsServiceImpl
    ):AnalyticService
}
/**如果某個(gè)類是由外部庫提供 ->Retrofit、OkHttpClient、Room數(shù)據(jù)庫等
 * 或者必須使用工具類的方式創(chuàng)建實(shí)例,也無法通過構(gòu)造函數(shù)注入。
  *解決方法->
 * 可以通過告知Hilt如何提供此類型的實(shí)例:在Hilt模塊類創(chuàng)建一個(gè)函數(shù)
 * 并使用@Provides為該函數(shù)添加注解。
 *
 *
 *帶有注解的函數(shù)會(huì)向Hilt提供一下信息:
 *   - 函數(shù)返回類型會(huì)告知Hilt提供哪個(gè)類型的實(shí)例
 *   - 函數(shù)參數(shù)會(huì)告知Hilt相應(yīng)類型的依賴類
 *   - 函數(shù)主體會(huì)告知Hilt如何提供相應(yīng)類型的實(shí)例。每當(dāng)需要提供該類型的實(shí)例時(shí),Hilt都會(huì)執(zhí)行函數(shù)主體。
 *
  */
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule{
    @Provides
    fun provideAnalyticsService():AnalyticService{

        return Retrofit.Builder()
                .baseUrl("www.google.com")
                .build()
                .create(AnalyticService::class.java)
    }
}

為同一類型提供多個(gè)綁定

使用限定符(Qualifiers) ->自定義注解??來標(biāo)識(shí)該類型的特定綁定。

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

這里以上述的例子繼續(xù)為例

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

您可以通過使用相應(yīng)的限定符為字段或參數(shù)添加注釋來注入所需的特定類型:

// 作為另一個(gè)類的依賴
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .client(okHttpClient)
               .build()
               .create(AnalyticsService::class.java)
  }
}

// 作為一個(gè)構(gòu)造函數(shù)注入類型的依賴
class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...

// 需要被注入的類
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {

  @AuthInterceptorOkHttpClient
  @Inject lateinit var okHttpClient: OkHttpClient
}

系統(tǒng)提供的預(yù)定義限定符

Hilt 提供了一些預(yù)定義的限定符。例如,由于您可能需要來自應(yīng)用或 Activity 的 Context 類,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

為Android類生成的組件

對(duì)于系統(tǒng)提供的每一個(gè)Android類,都有一個(gè)關(guān)聯(lián)的Hilt組件,您可以再@InstallIn注解中引用該組件。每個(gè)Hilt組件負(fù)責(zé)將其綁定注入相應(yīng)的Android類。
前面演示了如何再Hilt模塊中使用ActivityComponent。
Hilt提供了以下組件:

Hilt組件 注入器面向的對(duì)象
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent 帶有WithFragmentBindings注解的View
ServiceComponent Service

注意:Hilt不會(huì)為廣播接收器生成組件,因?yàn)镠ilt直接從ApplicationComponent注入廣播接收器


組件作用域

默認(rèn)情況下,Hilt中的所有綁定都未限定作用域 ->每當(dāng)應(yīng)用請(qǐng)求綁定時(shí),Hilt都會(huì)創(chuàng)建所需類型的一個(gè)新實(shí)例。
不過,Hilt也允許將綁定的作用域限定為特定組件。Hilt只為綁定作用域限定到的組件的每個(gè)實(shí)例創(chuàng)建一次限定作用域的綁定,對(duì)該綁定的所有請(qǐng)求共享同一實(shí)例。

Android類 生成的組件 作用域
Application ApplicationComponent @Singleton(單例)
ViewModel ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
帶有@WithFragmentBindings注解的View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

在這里,使用@ActivityScoped將AnalyticsAdapter的作用域限定為ActivityComponent,Hilt會(huì)在相應(yīng)Activity的真?zhèn)€生命周期內(nèi)提供AnalyticsAdapter的同一實(shí)例:

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

注意:將綁定的作用域限定為某個(gè)組件的作用域的成本可能很高,因?yàn)樘峁┑膶?duì)象在該組件被銷毀之前一直保留在內(nèi)存中。所以,在應(yīng)用中盡量少用限定作用域的綁定。如果綁定的內(nèi)部狀態(tài)要求在某一作用域內(nèi)使用同一實(shí)例,或者綁定的創(chuàng)建成本很高,那么將幫的作用域限定為某個(gè)組件是一種恰當(dāng)?shù)淖龇ā?/p>


組件默認(rèn)綁定

每個(gè)Hilt組件都附帶一組默認(rèn)綁定,Hilt可以將其作為依賴項(xiàng)注入您自己的自定義綁定。請(qǐng)注意,這些綁定對(duì)應(yīng)于常規(guī)Activity和Fragment類型,而不對(duì)應(yīng)于任何特定子類。因?yàn)椋琀ilt會(huì)使用單個(gè)Activity組件定義來注入所有Activity。每個(gè)Activity都有此組件的不同實(shí)例。

Android組件 默認(rèn)綁定
ApplicationComponent Application
ActivityRetainedComponent Application
ActivityComponent Application和Activity
FragmentComponent Application、
ViewComponent View
ViewWithFragmentComponent 帶有WithFragmentBindings注解的View
ServiceComponent Service

可以使用@ApplicationContext或@ActivityContext獲得應(yīng)用或Activity的上下文綁定。

class AnalyticsServiceImpl @Inject constructor(
  @ApplicationContext context: Context
) : AnalyticsService { ... }

// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
  application: Application
) : AnalyticsService { ... }

參考資料:https://developer.android.google.cn/reference?hl=zh_cn

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容