依賴注入
- Dependency Injection碳却,簡(jiǎn)稱DI拟杉;
- 依賴項(xiàng)注入可以使代碼解耦,便于復(fù)用弄屡,重構(gòu)和測(cè)試
什么是依賴項(xiàng)注入
- 類通常需要引用其他類题禀,可通過以下三種方式獲取所需的對(duì)象:
- 在類中創(chuàng)建所需依賴項(xiàng)的實(shí)例
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("是否有網(wǎng)絡(luò)連接:${networkCapabilities==null}")
- 以參數(shù)形式提供舞终,可以在構(gòu)造類時(shí)提供這些依賴項(xiàng)掏觉,或者將這些依賴項(xiàng)傳入需要各個(gè)依賴項(xiàng)的函數(shù);
Android中的依賴注入方式
手動(dòng)依賴注入
1. 構(gòu)造函數(shù)注入
//在構(gòu)造類時(shí)提供這些依賴項(xiàng)
class Phone (private val cpu: CPU) {
fun use() {
cpu.run()
}
}
val cpu = CPU()
val phone=Phone(cpu)
phone.use()
2. 字段注入(或 setter 注入)
- 依賴項(xiàng)將在創(chuàng)建類后實(shí)例化
//將依賴項(xiàng)傳入需要依賴項(xiàng)的函數(shù)
class Phone {
lateinit var cpu: CPU
fun use() = cpu.run()
}
val phone = Phone()
val cpu = CPU()
phone.cpu = cpu
phone.use()
- 上面兩種都是手動(dòng)依賴項(xiàng)注入迹鹅,但是如果依賴項(xiàng)和類過多担孔,手動(dòng)依賴注入就會(huì)產(chǎn)生一些問題
1. 使用越來越繁瑣;
2. 產(chǎn)生大量模板代碼江锨;
3. 必須按順序聲明依賴項(xiàng);
4. 很難重復(fù)使用對(duì)象;
自動(dòng)依賴注入框架
- 有一些庫(kù)通過自動(dòng)執(zhí)行創(chuàng)建和提供依賴項(xiàng)的過程解決此問題,實(shí)現(xiàn)原理有如下幾種方案:
1\. 通過反射糕篇,在運(yùn)行時(shí)連接依賴項(xiàng);
2\. 通過注解啄育,編譯時(shí)生成連接依賴項(xiàng)的代碼;
3\. kotlin 強(qiáng)大的語法糖和函數(shù)式編程;
1. Dagger:
- Android領(lǐng)域最廣為熟知的依賴注入框架,可以說大名鼎鼎了
Dagger 1.x版本:Square基于反射實(shí)現(xiàn)的拌消,有兩個(gè)缺點(diǎn)一個(gè)是反射的耗時(shí)挑豌,另一個(gè)是反射是運(yùn)行時(shí)的,編譯期不會(huì)報(bào)錯(cuò)墩崩。而使用難度較高氓英,剛接觸時(shí)有經(jīng)常容易寫錯(cuò),造成開發(fā)效率底鹦筹;
Dagger 2.x版本:Google基于Java注解實(shí)現(xiàn)的铝阐,完美解決了上述問題,
2. Koin
- 為 Kotlin 開發(fā)者提供的一個(gè)實(shí)用型輕量級(jí)依賴注入框架铐拐,采用純 Kotlin 語言編寫而成徘键,僅使用功能解析,無代理遍蟋、無代碼生成吹害、無反射(通過kotlin 強(qiáng)大的語法糖(例如 Inline、Reified 等等)和函數(shù)式編程實(shí)現(xiàn))虚青;
3. Hilt:
- 由于Dagger的復(fù)雜度和使用難度較大它呀,Android團(tuán)隊(duì)聯(lián)合Dagger2團(tuán)隊(duì),一起開發(fā)出來的一個(gè)專門面向Android的依賴注入框架Hilt,最明顯的特征就是:1. 簡(jiǎn)單钟些;2. 提供了Android專屬的API烟号;3. Google官方支持,和Jetpack其他組件配合使用政恍;
Hilt
- Hilt 通過為項(xiàng)目中的每個(gè) Android 類提供容器并自動(dòng)為您管理其生命周期,定義了一種在應(yīng)用中執(zhí)行 DI 的標(biāo)準(zhǔn)方法达传。
- Hilt 在熱門 DI 庫(kù) Dagger 的基礎(chǔ)上構(gòu)建而成篙耗,因而能夠受益于 Dagger 提供的編譯時(shí)正確性、運(yùn)行時(shí)性能宪赶、可伸縮性和 Android Studio 支持宗弯。
Hilt使用流程
添加依賴項(xiàng)
//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的依賴庫(kù)及Java8
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//對(duì)于 Kotlin 項(xiàng)目,需要添加 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 應(yīng)用類
- 用@HiltAndroidApp注解Application;
- @HiltAndroidApp注解 會(huì)觸發(fā) Hilt 的代碼生成操作搂妻,生成的代碼包括應(yīng)用的一個(gè)基類蒙保,該基類充當(dāng)應(yīng)用級(jí)依賴項(xiàng)容器;
@HiltAndroidApp
class MyApplication : MultiDexApplication() {
...
}
將依賴項(xiàng)注入 Android 類
1. 用@AndroidEntryPoint注釋類;
- 目前支持6類入口點(diǎn):Application(通過使用 @HiltAndroidApp),Activity欲主,F(xiàn)ragment邓厕,View,Service扁瓢,BroadcastReceiver
- 使用 @AndroidEntryPoint 注解 Android 類详恼,還必須為依賴于該類的 Android 類添加注釋,例如為注解 fragment 引几,則還必須為該 fragment 依賴的 Activity 添加@AndroidEntryPoint注釋昧互。
2. 使用 @Inject 注釋執(zhí)行字段
- @AndroidEntryPoint 會(huì)為項(xiàng)目中的每個(gè) Android 類生成一個(gè)單獨(dú)的 Hilt 組件。這些組件可以從它們各自的父類接收依賴項(xiàng), 如需從組件獲取依賴項(xiàng)伟桅,請(qǐng)使用 @Inject 注釋執(zhí)行字段注入敞掘, 注意:Hilt注入的字段是不可以聲明成private的;
3. 構(gòu)造函數(shù)中使用 @Inject 注釋
- 為了執(zhí)行字段注入楣铁,需要在類的構(gòu)造函數(shù)中使用 @Inject 注釋玖雁,以告知 Hilt 如何提供該類的實(shí)例:
@AndroidEntryPoint
class HiltDemoActivity : AppCompatActivity() {
@Inject
lateinit var cpu: CPU
...
}
class CPU @Inject constructor() {
var name: String = ""
fun run() {
LjyLogUtil.d("$name run...")
}
}
帶參數(shù)的依賴注入:
- 如果構(gòu)造函數(shù)中帶有參數(shù),Hilt要如何進(jìn)行依賴注入呢民褂?
- 需要構(gòu)造函數(shù)中所依賴的所有其他對(duì)象都支持依賴注入
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
- 有時(shí)一些類型參數(shù)不能通過構(gòu)造函數(shù)注入, 如 接口 或 來自外部庫(kù)的類茄菊,此時(shí)可以使用 Hilt模塊 向Hilt提供綁定信息;
- Hilt 模塊是一個(gè)帶有 @Module 注釋的類赊堪,并使用 @InstallIn 設(shè)置作用域
使用 @Binds 注入接口實(shí)例
//1\. 接口
interface ICPU {
fun run()
}
//2\. 實(shí)現(xiàn)類
class KylinCPU @Inject constructor() : ICPU {
override fun run() {
LjyLogUtil.d("kylin run...")
}
}
//3\. 被注入的類面殖,入?yún)⑹墙涌陬愋?class Phone @Inject constructor(val cpu: ICPU) {
fun use() {
cpu.run()
}
}
//4\. 使用@Binds注入接口實(shí)例
@Module
@InstallIn(ActivityComponent::class)
abstract class CPUModel {
@Binds
abstract fun bindCPU(cpu: KylinCPU): ICPU
}
//5\. 使用注入的實(shí)例
@AndroidEntryPoint
class HiltActivity : AppCompatActivity() {
@Inject
lateinit var phone: Phone
fun test() {
phone.cpu.name = "麒麟990"
phone.use()
}
}
使用 @Provides 注入實(shí)例
- 如果某個(gè)類不歸您所有(因?yàn)樗鼇碜酝獠繋?kù),如 Retrofit哭廉、OkHttpClient 或 Room 數(shù)據(jù)庫(kù)等類)脊僚,或者必須使用構(gòu)建器模式創(chuàng)建實(shí)例,也無法通過構(gòu)造函數(shù)注入。
@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()
}
}
為同一類型提供多個(gè)綁定
- 比如網(wǎng)絡(luò)請(qǐng)求中可能需要不同配置的OkHttpClient辽幌,或者不同BaseUrl的Retrofit
- 使用@Qualifier注解實(shí)現(xiàn)
//1\. 接口和實(shí)現(xiàn)類
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\. 創(chuàng)建多個(gè)類型的注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindKylinCPU
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindSnapdragonCPU
//@Retention:注解的生命周期
//AnnotationRetention.SOURCE:僅編譯期增淹,不存儲(chǔ)在二進(jìn)制輸出中
//AnnotationRetention.BINARY:存儲(chǔ)在二進(jìn)制輸出中,但對(duì)反射不可見
//AnnotationRetention.RUNTIME:存儲(chǔ)在二進(jìn)制輸出中乌企,對(duì)反射可見
//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\. 使用依賴注入獲取實(shí)例虑润,可以用在字段注解,也可以用在構(gòu)造函數(shù)或者方法入?yún)⒅?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()
}
}
組件默認(rèn)綁定
- 由于可能需要來自 Application 或 Activity 的 Context 類加酵,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符拳喻。
- 每個(gè) Hilt 組件都附帶一組默認(rèn)綁定,Hilt 可以將其作為依賴項(xiàng)注入您自己的自定義綁定
class Test1 @Inject constructor(@ApplicationContext private val context: Context)
class Test2 @Inject constructor(@ActivityContext private val context: Context)
- 對(duì)于Application和Activity這兩個(gè)類型猪腕,Hilt也是給它們預(yù)置好了注入功能(必須是這兩個(gè)冗澈,即使子類也不可以)
class Test3 @Inject constructor(val application: Application)
class Test4 @Inject constructor(val activity: Activity)
Hilt內(nèi)置組件類型
- 上面使用Hilt Module時(shí),有用到@InstallIn(), 意思是把這個(gè)模塊安裝到哪個(gè)組件中
Hilt內(nèi)置了7種組件可選:
- ApplicationComponent:對(duì)應(yīng)Application陋葡,依賴注入實(shí)例可以在全項(xiàng)目中使用
- ActivityRetainedComponent:對(duì)應(yīng)ViewModel(在配置更改后仍然存在亚亲,因此它在第一次調(diào)用 Activity#onCreate() 時(shí)創(chuàng)建,在最后一次調(diào)用 Activity#onDestroy() 時(shí)銷毀)
- ActivityComponent:對(duì)應(yīng)Activity腐缤,Activity中包含的Fragment和View也可以使用捌归;
- FragmentComponent:對(duì)應(yīng)Fragment
- ViewComponent:對(duì)應(yīng)View
- ViewWithFragmentComponent:對(duì)應(yīng)帶有 @WithFragmentBindings 注釋的 View
- ServiceComponent:對(duì)應(yīng)Service
- Hilt 沒有為 broadcast receivers 提供組件,因?yàn)?Hilt 直接從 ApplicationComponent 注入 broadcast receivers柴梆;
組件作用域
- Hilt默認(rèn)會(huì)為每次的依賴注入行為都創(chuàng)建不同的實(shí)例陨溅。
Hilt內(nèi)置7種組件作用域注解
- @Singleton:對(duì)應(yīng)組件ApplicationComponent,整個(gè)項(xiàng)目共享同一個(gè)實(shí)例
- @ActivityRetainedScope:對(duì)應(yīng)組件ActivityRetainedComponent
- @ActivityScoped:對(duì)應(yīng)組件ActivityComponent绍在,在同一個(gè)Activity(包括其包含的Fragment和View中)內(nèi)部將會(huì)共享同一個(gè)實(shí)例
- @FragmentScoped:對(duì)應(yīng)組件FragmentComponent
- @ViewScoped:對(duì)應(yīng)組件ViewComponent和ViewWithFragmentComponent门扇;
- @ServiceScopedService:對(duì)應(yīng)ServiceComponent
- 比如我們經(jīng)常會(huì)需要一個(gè)全局的OkhttpClient或者Retrofit,就可以如下實(shí)現(xiàn)
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默認(rèn)會(huì)為每次的依賴注入行為都創(chuàng)建不同的實(shí)例。
@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操作本地?cái)?shù)據(jù)庫(kù)
@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的依賴注入
- 通過上面的學(xué)習(xí)偿渡,那我們?nèi)绻赩iewModel中創(chuàng)建Repository要如何實(shí)現(xiàn)呢臼寄,可以如下:
//1\. 倉(cāng)庫(kù)層
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<String> = 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實(shí)例的常規(guī)方式, 為此Hilt專門為其提供了一種獨(dú)立的依賴注入方式: @ViewModelInject
//1\. 添加兩個(gè)額外的依賴
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獲取改為常規(guī)寫法
@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被銷毀一般有三種情況:
- 界面關(guān)閉或退出應(yīng)用
- Activity 配置 (configuration) 被改變溜宽,如旋轉(zhuǎn)屏幕時(shí)吉拳;
- 在后臺(tái)時(shí)因運(yùn)行內(nèi)存不足被系統(tǒng)回收;
- ViewModel 會(huì)處理2的情況适揉,而3的情況就需要使用onSaveInstanceState()保存數(shù)據(jù)留攒,重建時(shí)用SavedStateHandle恢復(fù)數(shù)據(jù),就要用@assist 注解添加 SavedStateHandle 依賴項(xiàng)
class MyViewModel @ViewModelInject constructor(
private val repository: Repository,
//SavedStateHandle 用于進(jìn)程被終止時(shí)嫉嘀,保存和恢復(fù)數(shù)據(jù)
@Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var result: MutableLiveData<String> = MutableLiveData()
private val userId: MutableLiveData<String> = 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 不支持的類中注入依賴項(xiàng)
- 可以使用 @EntryPoint 注釋創(chuàng)建入口點(diǎn), 調(diào)用EntryPointAccessors的靜態(tài)方法來獲得自定義入口點(diǎn)的實(shí)例
- EntryPointAccessors提供了四個(gè)靜態(tài)方法:fromActivity炼邀、fromApplication、fromFragment剪侮、fromView拭宁,根據(jù)自定義入口的MyEntryPoint的注解@InstallIn所指定的范圍選擇對(duì)應(yīng)的獲取方法;
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface MyEntryPoint{
fun getRetrofit():Retrofit
}
在ContentProvider中使用
- Hilt支持的入口點(diǎn)中少了一個(gè)關(guān)鍵的Android組件:ContentProvider, 主要原因就是ContentProvider.onCreate() 在Application的onCreate() 之前執(zhí)行,因此很多人會(huì)利用這個(gè)特性去進(jìn)行提前初始化, 詳見Android Jetpack系列--5. App Startup使用詳解, 而Hilt的工作原理是從Application.onCreate()中開始的杰标,即ContentProvider.onCreate()執(zhí)行之前兵怯,Hilt的所有功能都還無法正常工作;
class MyContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
context?.let {
val appContext=it.applicationContext
//調(diào)用EntryPointAccessors.fromApplication()函數(shù)來獲得自定義入口點(diǎn)的實(shí)例
val entryPoint=EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java)
//再調(diào)用入口點(diǎn)中定義的getRetrofit()函數(shù)就能得到Retrofit的實(shí)例
val retrofit=entryPoint.getRetrofit()
LjyLogUtil.d("retrofit:$retrofit")
}
return true
}
...
}
在 App Startup 中使用
- App Startup 會(huì)默認(rèn)提供一個(gè) InitializationProvider腔剂,InitializationProvider 繼承 ContentProvider媒区;
class LjyInitializer : Initializer<Unit> {
override fun create(context: Context) {
//調(diào)用EntryPointAccessors.fromApplication()函數(shù)來獲得自定義入口點(diǎn)的實(shí)例
val entryPoint= EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
//再調(diào)用入口點(diǎn)中定義的getRetrofit()函數(shù)就能得到Retrofit的實(shí)例
val retrofit=entryPoint.getRetrofit()
LjyLogUtil.d("retrofit:$retrofit")
}
override fun dependencies(): List<Class<out Initializer<*>>> {
return emptyList()
}
}
報(bào)錯(cuò)解決
- 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.