引入
在了解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 { ... }