Android Jetpack系列--9. Hilt使用詳解

語言: CN / TW / HK

相關知識

依賴注入

  • Dependency Injection,簡稱DI;
  • 依賴項注入可以使程式碼解耦,便於複用,重構和測試

什麼是依賴項注入

  • 類通常需要引用其他類,可通過以下三種方式獲取所需的物件:
  • 在類中建立所需依賴項的例項 class CPU () { var name: String = "" fun run() { LjyLogUtil.d("$name run...") } } class Phone1 { val cpu = CPU() fun use() { cpu.run() } }
  • 通過父類或其他類獲取 val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val networkCapabilities = cm.getNetworkCapabilities(cm.activeNetwork) LjyLogUtil.d("是否有網路連線:${networkCapabilities==null}")
  • 以引數形式提供,可以在構造類時提供這些依賴項,或者將這些依賴項傳入需要各個依賴項的函式;
  • 第三種方式就是依賴項注入

Android中的依賴注入方式

手動依賴注入
1. 建構函式注入

//在構造類時提供這些依賴項 class Phone (private val cpu: CPU) { fun use() { cpu.run() } } val cpu = CPU() val phone=Phone(cpu) phone.use()

2. 欄位注入(或 setter 注入)
  • 依賴項將在建立類後例項化 //將依賴項傳入需要依賴項的函式 class Phone { lateinit var cpu: CPU fun use() = cpu.run() } val phone = Phone() val cpu = CPU() phone.cpu = cpu phone.use()
  • 上面兩種都是手動依賴項注入,但是如果依賴項和類過多,手動依賴注入就會產生一些問題 ```
  • 使用越來越繁瑣;
  • 產生大量模板程式碼;
  • 必須按順序宣告依賴項;
  • 很難重複使用物件; ```
自動依賴注入框架
  • 有一些庫通過自動執行建立和提供依賴項的過程解決此問題,實現原理有如下幾種方案: ```
  • 通過反射,在執行時連線依賴項;
  • 通過註解,編譯時生成連線依賴項的程式碼;
  • kotlin 強大的語法糖和函數語言程式設計; ```
1. Dagger:
  • Android領域最廣為熟知的依賴注入框架,可以說大名鼎鼎了 Dagger 1.x版本:Square基於反射實現的,有兩個缺點一個是反射的耗時,另一個是反射是執行時的,編譯期不會報錯。而使用難度較高,剛接觸時有經常容易寫錯,造成開發效率底; Dagger 2.x版本:Google基於Java註解實現的,完美解決了上述問題,
2. Koin
  • 為 Kotlin 開發者提供的一個實用型輕量級依賴注入框架,採用純 Kotlin 語言編寫而成,僅使用功能解析,無代理、無程式碼生成、無反射(通過kotlin 強大的語法糖(例如 Inline、Reified 等等)和函數語言程式設計實現);
3. Hilt:
  • 由於Dagger的複雜度和使用難度較大,Android團隊聯合Dagger2團隊,一起開發出來的一個專門面向Android的依賴注入框架Hilt,最明顯的特徵就是:1. 簡單;2. 提供了Android專屬的API;3. Google官方支援,和Jetpack其他元件配合使用;

Hilt

  • Hilt 通過為專案中的每個 Android 類提供容器並自動為您管理其生命週期,定義了一種在應用中執行 DI 的標準方法。
  • Hilt 在熱門 DI 庫 Dagger 的基礎上構建而成,因而能夠受益於 Dagger 提供的編譯時正確性、執行時效能、可伸縮性和 Android Studio 支援。

Hilt使用流程

新增依賴項

//1. 配置Hilt的外掛路徑 buildscript { ... dependencies { ... classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha' } } //2. 引入Hilt的外掛 plugins { ... id 'dagger.hilt.android.plugin' id 'kotlin-kapt' } //3. 新增Hilt的依賴庫及Java8 android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } //對於 Kotlin 專案,需要新增 kotlinOptions kotlinOptions { jvmTarget = "1.8" } } dependencies { ... implementation "com.google.dagger:hilt-android:2.28-alpha" kapt "com.google.dagger:hilt-android-compiler:2.28-alpha" }

Hilt 應用類

  • 用@HiltAndroidApp註解Application;
  • @HiltAndroidApp註解 會觸發 Hilt 的程式碼生成操作,生成的程式碼包括應用的一個基類,該基類充當應用級依賴項容器; @HiltAndroidApp class MyApplication : MultiDexApplication() { ... }

將依賴項注入 Android 類

1. 用@AndroidEntryPoint註釋類;
  • 目前支援6類入口點:Application(通過使用 @HiltAndroidApp),Activity,Fragment,View,Service,BroadcastReceiver
  • 使用 @AndroidEntryPoint 註解 Android 類,還必須為依賴於該類的 Android 類添加註釋,例如為註解 fragment ,則還必須為該 fragment 依賴的 Activity 新增@AndroidEntryPoint註釋。
2. 使用 @Inject 註釋執行欄位
  • @AndroidEntryPoint 會為專案中的每個 Android 類生成一個單獨的 Hilt 元件。這些元件可以從它們各自的父類接收依賴項, 如需從元件獲取依賴項,請使用 @Inject 註釋執行欄位注入, 注意:Hilt注入的欄位是不可以宣告成private的;
3. 建構函式中使用 @Inject 註釋
  • 為了執行欄位注入,需要在類的建構函式中使用 @Inject 註釋,以告知 Hilt 如何提供該類的例項: ``` @AndroidEntryPoint class HiltDemoActivity : AppCompatActivity() { @Inject lateinit var cpu: CPU ... }

class CPU @Inject constructor() { var name: String = ""

fun run() {
    LjyLogUtil.d("$name run...")
}

} ```

帶引數的依賴注入:
  • 如果建構函式中帶有引數,Hilt要如何進行依賴注入呢?
  • 需要建構函式中所依賴的所有其他物件都支援依賴注入 ``` class CPU @Inject constructor() { var name: String = "" fun run() { LjyLogUtil.d("$name run...") } }

class Phone @Inject constructor(val cpu: CPU) { fun use() { cpu.run() } }

@AndroidEntryPoint class HiltActivity : AppCompatActivity() { @Inject lateinit var phone: Phone

fun test() {
    phone.cpu.name = "麒麟990"
    phone.use()
}

} ```

Hilt Module

  • 有時一些型別引數不能通過建構函式注入, 如 介面 或 來自外部庫的類,此時可以使用 Hilt模組 向Hilt提供繫結資訊;
  • Hilt 模組是一個帶有 @Module 註釋的類,並使用 @InstallIn 設定作用域
使用 @Binds 注入介面例項

``` //1. 介面 interface ICPU { fun run() } //2. 實現類 class KylinCPU @Inject constructor() : ICPU { override fun run() { LjyLogUtil.d("kylin run...") } } //3. 被注入的類,入參是介面型別 class Phone @Inject constructor(val cpu: ICPU) { fun use() { cpu.run() } } //4. 使用@Binds注入介面例項 @Module @InstallIn(ActivityComponent::class) abstract class CPUModel { @Binds abstract fun bindCPU(cpu: KylinCPU): ICPU } //5. 使用注入的例項 @AndroidEntryPoint class HiltActivity : AppCompatActivity() { @Inject lateinit var phone: Phone

fun test() {
    phone.cpu.name = "麒麟990"
    phone.use()
}

} ```

使用 @Provides 注入例項
  • 如果某個類不歸您所有(因為它來自外部庫,如 Retrofit、OkHttpClient 或 Room 資料庫等類),或者必須使用構建器模式建立例項,也無法通過建構函式注入。 @Module @InstallIn(ApplicationComponent::class) class NetworkModel { @Provides fun provideOkHttpClient(): OkHttpClient { return OkHttpClient().newBuilder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(90, TimeUnit.SECONDS) .build() } }
為同一型別提供多個繫結
  • 比如網路請求中可能需要不同配置的OkHttpClient,或者不同BaseUrl的Retrofit
  • 使用@Qualifier註解實現 ``` //1. 介面和實現類 interface ICPU { fun run() }

class KylinCPU @Inject constructor() : ICPU { override fun run() { LjyLogUtil.d("kylin run...") } }

class SnapdragonCPU @Inject constructor() : ICPU { override fun run() { LjyLogUtil.d("snapdragon run...") } }

//2. 建立多個型別的註解 @Qualifier @Retention(AnnotationRetention.BINARY) annotation class BindKylinCPU

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

//@Retention:註解的生命週期 //AnnotationRetention.SOURCE:僅編譯期,不儲存在二進位制輸出中 //AnnotationRetention.BINARY:儲存在二進位制輸出中,但對反射不可見 //AnnotationRetention.RUNTIME:儲存在二進位制輸出中,對反射可見

//3. 在Hilt模組中使用註解 @Module @InstallIn(ActivityComponent::class) abstract class CPUModel { @BindKylinCPU @Binds abstract fun bindKylinCPU(cpu: KylinCPU): ICPU

@BindSnapdragonCPU
@Binds
abstract fun bindSnapdragonCPU(cpu: SnapdragonCPU): ICPU

}

//4. 使用依賴注入獲取例項,可以用在欄位註解,也可以用在建構函式或者方法入參中 class Phone5 @Inject constructor(@BindSnapdragonCPU private val cpu: ICPU) { @BindKylinCPU @Inject lateinit var cpu1: ICPU

@BindSnapdragonCPU
@Inject
lateinit var cpu2: ICPU

fun use() {
    cpu.run()
    cpu1.run()
    cpu2.run()
}

fun use(@BindKylinCPU cpu: ICPU) {
    cpu.run()
}

} ```

元件預設繫結

  • 由於可能需要來自 Application 或 Activity 的 Context 類,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
  • 每個 Hilt 元件都附帶一組預設繫結,Hilt 可以將其作為依賴項注入您自己的自定義繫結 ``` class Test1 @Inject constructor(@ApplicationContext private val context: Context)

class Test2 @Inject constructor(@ActivityContext private val context: Context) - 對於Application和Activity這兩個型別,Hilt也是給它們預置好了注入功能(必須是這兩個,即使子類也不可以) class Test3 @Inject constructor(val application: Application)

class Test4 @Inject constructor(val activity: Activity) ```

Hilt內建元件型別

  • 上面使用Hilt Module時,有用到@InstallIn(), 意思是把這個模組安裝到哪個元件中
Hilt內建了7種元件可選:
  1. ApplicationComponent:對應Application,依賴注入例項可以在全專案中使用
  2. ActivityRetainedComponent:對應ViewModel(在配置更改後仍然存在,因此它在第一次呼叫 Activity#onCreate() 時建立,在最後一次呼叫 Activity#onDestroy() 時銷燬)
  3. ActivityComponent:對應Activity,Activity中包含的Fragment和View也可以使用;
  4. FragmentComponent:對應Fragment
  5. ViewComponent:對應View
  6. ViewWithFragmentComponent:對應帶有 @WithFragmentBindings 註釋的 View
  7. ServiceComponent:對應Service
  8. Hilt 沒有為 broadcast receivers 提供元件,因為 Hilt 直接從 ApplicationComponent 注入 broadcast receivers;

元件作用域

  • Hilt預設會為每次的依賴注入行為都建立不同的例項。
Hilt內建7種元件作用域註解
  1. @Singleton:對應元件ApplicationComponent,整個專案共享同一個例項
  2. @ActivityRetainedScope:對應元件ActivityRetainedComponent
  3. @ActivityScoped:對應元件ActivityComponent,在同一個Activity(包括其包含的Fragment和View中)內部將會共享同一個例項
  4. @FragmentScoped:對應元件FragmentComponent
  5. @ViewScoped:對應元件ViewComponent和ViewWithFragmentComponent;
  6. @ServiceScopedService:對應ServiceComponent
  7. 比如我們經常會需要一個全域性的OkhttpClient或者Retrofit,就可以如下實現 ``` interface ApiService { @GET("search/repositories?sort=stars&q=Android") suspend fun searRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse }

@Module @InstallIn(ApplicationComponent::class) class NetworkModel {

companion object {
    private const val BASE_URL = "https://api.github.com/"
}

@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
    return retrofit.create(ApiService::class.java)
}

//元件作用域:Hilt預設會為每次的依賴注入行為都建立不同的例項。
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .build()
}

@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
    return OkHttpClient().newBuilder()
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(90, TimeUnit.SECONDS)
        .build()
}

} - 或是使用Room操作本地資料庫 @Database(entities = [RepoEntity::class], version = Constants.DB_VERSION) abstract class AppDatabase : RoomDatabase() { abstract fun repoDao(): RepoDao }

@Module @InstallIn(ApplicationComponent::class) object RoomModule { @Provides @Singleton fun provideAppDatabase(application: Application): AppDatabase { return Room .databaseBuilder( application.applicationContext, AppDatabase::class.java, Constants.DB_NAME ) .allowMainThreadQueries() //允許在主執行緒中查詢 .build() }

@Provides
@Singleton
fun provideRepoDao(appDatabase: AppDatabase):RepoDao{
    return appDatabase.repoDao()
}

} ```

ViewModel的依賴注入

  • 通過上面的學習,那我們如果在ViewModel中建立Repository要如何實現呢,可以如下: ``` //1. 倉庫層 class Repository @Inject constructor(){ @Inject lateinit var apiService: ApiService suspend fun getData(): RepoResponse { return apiService.searRepos(1, 5) } } //2. ViewModel層 @ActivityRetainedScoped class MyViewModel @Inject constructor(private val repository: Repository): ViewModel() { var result: MutableLiveData = MutableLiveData() fun doWork() { viewModelScope.launch { runCatching { withContext(Dispatchers.IO){ repository.getData() } }.onSuccess { result.value="RepoResponse=${gson().toJson(it)}" }.onFailure { result.value=it.message } } } } //3. Activity層 @AndroidEntryPoint class HiltMvvmActivity : AppCompatActivity() {

    @Inject lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hilt_mvvm) viewModel.result.observe(this, Observer { LjyLogUtil.d("result:$it") }) lifecycleScope viewModel.doWork() } } ```

    ViewModel 和 @ViewModelInject 註解
    • 這種改變了獲取ViewModel例項的常規方式, 為此Hilt專門為其提供了一種獨立的依賴注入方式: @ViewModelInject //1. 新增兩個額外的依賴 implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02' kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02' //2. 修改MyViewModel: 去掉@ActivityRetainedScoped註解,把@Inject改為@ViewModelInject class MyViewModel @ViewModelInject constructor(private val repository: Repository): ViewModel() { ... } //3. activity中viewModel獲取改為常規寫法 @AndroidEntryPoint class HiltMvvmActivity : AppCompatActivity() { // @Inject // lateinit var viewModel: MyViewModel val viewModel: MyViewModel by viewModels() // 或 val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) } ... }
    SavedStateHandle 和 @assist註解
    • Activity/Fragment被銷燬一般有三種情況:
    • 介面關閉或退出應用
    • Activity 配置 (configuration) 被改變,如旋轉螢幕時;
    • 在後臺時因執行記憶體不足被系統回收;
    • ViewModel 會處理2的情況,而3的情況就需要使用onSaveInstanceState()儲存資料,重建時用SavedStateHandle恢復資料,就要用@assist 註解新增 SavedStateHandle 依賴項 ``` class MyViewModel @ViewModelInject constructor( private val repository: Repository, //SavedStateHandle 用於程序被終止時,儲存和恢復資料 @Assisted private val savedStateHandle: SavedStateHandle ) : ViewModel() { var result: MutableLiveData = MutableLiveData() private val userId: MutableLiveData = savedStateHandle.getLiveData("userId")

    fun doWork() { viewModelScope.launch { runCatching { withContext(Dispatchers.IO) { repository.getData(userId) } }.onSuccess { result.value = "RepoResponse=${Gson().toJson(it)}" }.onFailure { result.value = it.message } } } } ```

    在 Hilt 不支援的類中注入依賴項

    • 可以使用 @EntryPoint 註釋建立入口點, 呼叫EntryPointAccessors的靜態方法來獲得自定義入口點的例項
    • EntryPointAccessors提供了四個靜態方法:fromActivity、fromApplication、fromFragment、fromView,根據自定義入口的MyEntryPoint的註解@InstallIn所指定的範圍選擇對應的獲取方法; @EntryPoint @InstallIn(ApplicationComponent::class) interface MyEntryPoint{ fun getRetrofit():Retrofit }
    在ContentProvider中使用
    • Hilt支援的入口點中少了一個關鍵的Android元件:ContentProvider, 主要原因就是ContentProvider.onCreate() 在Application的onCreate() 之前執行,因此很多人會利用這個特性去進行提前初始化, 詳見Android Jetpack系列--5. App Startup使用詳解, 而Hilt的工作原理是從Application.onCreate()中開始的,即ContentProvider.onCreate()執行之前,Hilt的所有功能都還無法正常工作; class MyContentProvider : ContentProvider() { override fun onCreate(): Boolean { context?.let { val appContext=it.applicationContext //呼叫EntryPointAccessors.fromApplication()函式來獲得自定義入口點的例項 val entryPoint=EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java) //再呼叫入口點中定義的getRetrofit()函式就能得到Retrofit的例項 val retrofit=entryPoint.getRetrofit() LjyLogUtil.d("retrofit:$retrofit") } return true } ... }
    在 App Startup 中使用
    • App Startup 會預設提供一個 InitializationProvider,InitializationProvider 繼承 ContentProvider; ``` class LjyInitializer : Initializer { override fun create(context: Context) { //呼叫EntryPointAccessors.fromApplication()函式來獲得自定義入口點的例項 val entryPoint= EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java) //再呼叫入口點中定義的getRetrofit()函式就能得到Retrofit的例項 val retrofit=entryPoint.getRetrofit() LjyLogUtil.d("retrofit:$retrofit") }

    override fun dependencies(): List<Class<out Initializer<*>>> { return emptyList() } } ```

    報錯解決

    • Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin? android { ... defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/schemas".toString()] } } } } //If so, try changing from "arguments =" to "arguments +=", as just using equals overwrites anything set previously.

我是今陽,如果想要進階和了解更多的乾貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章