Android Jetpack系列--9. Hilt使用詳解
相關知識
依賴注入
- 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種元件可選:
- ApplicationComponent:對應Application,依賴注入例項可以在全專案中使用
- ActivityRetainedComponent:對應ViewModel(在配置更改後仍然存在,因此它在第一次呼叫 Activity#onCreate() 時建立,在最後一次呼叫 Activity#onDestroy() 時銷燬)
- ActivityComponent:對應Activity,Activity中包含的Fragment和View也可以使用;
- FragmentComponent:對應Fragment
- ViewComponent:對應View
- ViewWithFragmentComponent:對應帶有 @WithFragmentBindings 註釋的 View
- ServiceComponent:對應Service
- Hilt 沒有為 broadcast receivers 提供元件,因為 Hilt 直接從 ApplicationComponent 注入 broadcast receivers;
元件作用域
- Hilt預設會為每次的依賴注入行為都建立不同的例項。
Hilt內建7種元件作用域註解
- @Singleton:對應元件ApplicationComponent,整個專案共享同一個例項
- @ActivityRetainedScope:對應元件ActivityRetainedComponent
- @ActivityScoped:對應元件ActivityComponent,在同一個Activity(包括其包含的Fragment和View中)內部將會共享同一個例項
- @FragmentScoped:對應元件FragmentComponent
- @ViewScoped:對應元件ViewComponent和ViewWithFragmentComponent;
- @ServiceScopedService:對應ServiceComponent
- 比如我們經常會需要一個全域性的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 = "http://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.
- 這種改變了獲取ViewModel例項的常規方式, 為此Hilt專門為其提供了一種獨立的依賴注入方式: @ViewModelInject
我是今陽,如果想要進階和了解更多的乾貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章
- Android Jetpack系列--9. Hilt使用詳解
- Android高手筆記-D8, R8編譯優化
- 探索Android開源框架 - 11. 熱修復原理
- 探索Android開源框架 - 11. 熱修復原理
- Android高手筆記-D8, R8編譯優化
- Android進階筆記-5. IPC機制 & Binder 原理
- Android進階筆記-4. BroadcastReceiver的註冊、傳送和接收
- 重學Java系列-2. JVM記憶體模型 & 類載入機制
- 重學Java系列-1. GC原理 & 垃圾回收演算法
- 幹掉RxJava系列--1. 手寫許可權請求替代RxPermission
- 探索Android開源框架 - 10. 外掛化原理
- 探索Android開源框架 - 9. ARouter使用及原始碼解析
- 探索Android開源框架 - 8. Gson使用及原始碼解析
- 探索Android開源框架 - 7. LeakCanary使用及原始碼解析
- 探索Android開源框架 - 7. LeakCanary使用及原始碼解析
- 探索Android開源框架 - 6. ButterKnife使用及原始碼解析
- 探索Android開源框架 - 5. EventBus使用及原始碼解析
- 探索Android開源框架 - 4. Glide使用及原始碼解析
- 探索Android開源框架 - 4. Glide使用及原始碼解析
- 探索Android開源框架 - 3. RxJava使用及原始碼解析