引入
在了解Hilt之前,我們先來了解一個重要的概念——依賴注入(以下來源:Android開發者文檔)
依賴注入(DI)是一種廣泛用于編程的技術,非常適用于Android開發。遵循DI的原則可以良好的應用架構奠定基礎。
實現依賴注入可以帶來以下優勢:1、重用代碼;2、易于重構;3、易于測試
什么是依賴項注入
類通常需要引用其他類。例如Car類可能需要引用Engine類。這些必需類成為依賴項,在此實例中,Car類依賴于擁有Engine類的實例才能實現。
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
類通常有三種方式獲取所需的對象:
1、類構造其所需的依賴項。在以上示例中,Car將創建并初始化自己的Engine實例。
2、從其他地方抓取。某些Android API(如getSystemService())的工作原理就是如此。
3、以參數形式提供。應用可以在構造類時提供這些依賴項,或者將這些依賴項傳入需要各個依賴項的函數。在以上示例中,Car構造函數將接收Engine作為參數。這就是所謂的依賴項注入,使用這種方法,我們可以獲取并提供類的依賴項,而不必讓類實例自行獲取。
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
使用這種方法的好處顯而易見:作為Car 的構造函數參數,可以輕松地進行拆卸并測試其他類(Engine的子類)。
Android中有兩種主要的手動依賴項注入方式:
- 構造函數注入,也就是上方提及的方式
- 字段注入(或setter注入)。某些Android框架類(如Activity和Fragment)由系統實例化,因此無法進行構造函數注入。使用字段注入時,依賴項將在創建類后實例化。
說的那么高端,其實就是延遲加載
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
注:依賴想注入基于控制反轉原則,根據該原則,通用代碼控制特定的執行。
依賴性注入的替代方法
依賴項注入的替代方法是使用服務定位器(或者我們一般更喜歡叫工具類)。服務定位器設計模式還改進了類與具體依賴項的分離。可以創建一個名為服務定位器的類,該類創建和存儲依賴項,然后按需提供這些依賴項。
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()
}
服務定位器模式與依賴項注入在元素使用方式上有所不同。使用服務定位器模式,類可以控制并請求注入對象;使用依賴想注入,應用可以控制并主動注入所需對象。
手動實現依賴項注入的實戰案例
這是Android開發平臺提供的一個引入Hilt的一個例子,里面從最初的依賴注入講起,逐步規范依賴注入模式,最后達到和Dagger相接近的案例。
https://developer.android.google.cn/training/dependency-injection/manual?hl=zh_cn
其中涉及到MVVM涉及模式架構的搭建,在我往期的實戰項目中也有體現。
總結:
依賴項注入對于創建可擴展且可測試的Android應用而言是一項適合的技術。將容器作為在應用的不同部分共享各個類實例的一種方式,以及使用工廠類創建各個類實例的集中位置。
當應用變大時,我們會發現我們的項目編寫了大量樣板代碼(如工廠類),這可能容易出錯。我們還必須自行管理容器的范圍和生命周期,優化并舍棄不再需要的容器以釋放內存。如果操作不當,可能會呆滯應用出現微小錯誤和內存泄漏。
使用Hilt實現依賴注入
Hilt是Android的依賴項注入庫,可減少在項目中執行手動依賴項注入的樣板代碼。執行手動依賴項注入要求我們手動構造每個類機器依賴項,并借助容器重復使用和管理依賴項。
Hilt通過為項目中的每個Android類提供容器并自動管理其生命周期,提供了一種在應用中使用DI(依賴項注入)的標準方法。Hilt是在Dagger的基礎上構建而成的。
添加依賴
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
如果這里在配置的時候報錯,將單引號更改為雙引號即可。
...
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的應用都必須包含一個帶有@HiltAndroidApp注解的Application類。
@HiltAndroidApp
class MyApplication:Application() {
}
為什么需要一個Application并且需要添加注解@HiltAndroidApp?
@HiltAndroidApp會觸發Hilt的代碼生成操作,生成的代碼包括應用的一個基類,該基類充當應用級依賴項容器。
生成的這一Hilt組件會附加到Application對象的聲明周期,并為其提供依賴項。此外,它也是應用的父組件,這意味著,其他組件可以訪問它提供的依賴項。
將依賴注入Android類
在Application類中設置了Hilt且有了應用及組建后,Hilt可以為帶有@AndroidEntryPoint的其他Android類提供依賴項
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
如果使用@AndroidEntryPoint為某個Android類添加注解,則必須為依賴于該類的Android類添加注解。例如,如果我們為某個Fragment添加注解,則必須為使用該Fragment的所有Activity添加注解。
注:由Hilt注入的字段不能為私有字段。會導致編譯報錯
@AndroidEntryPoint會為項目中的每個Android類生成一個單獨的Hilt組件。這些組件可以從它們各自的父類接收依賴項。
如需從組件獲取依賴項,需使用@inject注解執行字段注入:
@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綁定
/**
* 為了執行字段注入,Hilt需要知道如何從相應組件提供必要依賴項的實例 ->綁定
* 綁定包含將某個類型的實例作為依賴項提供所需的信息。
* 向Hilt提供綁定信息的方法是構造函數注入。在某個類的構造函數中使用@Inject注解,
* 以告知Hilt如何提供該類的實例
*
* Student是MyAdapter的一個依賴項,所以Hilt必須知道如何提供Student的實例
*/
class MyAdapter @Inject constructor(private val student: Student) {
}
Hilt模塊
有時,類型不能通過構造函數注入。發生這種情況的原因有很多。例如,您不能通過構造函數注入接口。此外,您也不能通過構造函數注入不歸您所有的類型,如來自外部庫的類。在這些情況下,您可以通過使用Hilt模塊向Hilt提供綁定信息。
Hilt模塊是一個帶有@Module注解的類。 它會告知Hilt如何提供某些類型的實例。還必須使用@InstallIn為Hilt模塊添加注解,以告知Hilt每個模塊將用在或安裝在哪個Android類中。
在Hilt模塊中提供的依賴項可以在生成的所有與Hilt模塊安裝到的Android類關聯的組件中使用。
注:由于Hilt的代碼生成操作需要訪問使用Hilt 的所有Gradle模塊,因此編譯Application類的Gradle模塊還需要在其傳遞依賴項中包含您的所有Hilt模塊和通過構造函數注入的類。
/**
*@Description
*@Author PC
*@QQ 1578684787
*/
/**
* 使用@Binds注入接口實例
* 接口是無法通過構造函數注入的,而應該向Hilt提供綁定信息
* -> 在Hilt模塊內創建一個帶有@Binds注解的抽象函數
* @Binds 注解會告知Hilt在需要提供接口的實例時要使用哪種實現
* 帶有注解的函數回想Hilt提供以下信息:
* - 函數返回類型會告知Hilt函數提供哪個接口的實例。
* - 函數參數會告知Hilt要提供哪種實現。
*/
//1、需要一個接口
interface AnalyticService{
fun analyticsMethods()
}
//2、接口實現
//Hilt也需要知道如何提供AnalyticsServiceImpl的實例
class AnalyticsServiceImpl @Inject constructor():AnalyticService {
override fun analyticsMethods() {
}
}
//@InstallIn(ActivityComponent::class) 意味著所有依賴項都可以在應用的Activity中使用,
// 同理還有ApplicationComponent
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsNodule{
//3、注入接口實現
//提供接口類型的返回值,體現多態的作用,返回值可以切換任意繼承該接口的類的參數
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
):AnalyticService
}
/**如果某個類是由外部庫提供 ->Retrofit、OkHttpClient、Room數據庫等
* 或者必須使用工具類的方式創建實例,也無法通過構造函數注入。
*解決方法->
* 可以通過告知Hilt如何提供此類型的實例:在Hilt模塊類創建一個函數
* 并使用@Provides為該函數添加注解。
*
*
*帶有注解的函數會向Hilt提供一下信息:
* - 函數返回類型會告知Hilt提供哪個類型的實例
* - 函數參數會告知Hilt相應類型的依賴類
* - 函數主體會告知Hilt如何提供相應類型的實例。每當需要提供該類型的實例時,Hilt都會執行函數主體。
*
*/
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule{
@Provides
fun provideAnalyticsService():AnalyticService{
return Retrofit.Builder()
.baseUrl("www.google.com")
.build()
.create(AnalyticService::class.java)
}
}
為同一類型提供多個綁定
使用限定符(Qualifiers) ->自定義注解??來標識該類型的特定綁定。
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient
這里以上述的例子繼續為例
@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()
}
}
您可以通過使用相應的限定符為字段或參數添加注釋來注入所需的特定類型:
// 作為另一個類的依賴
@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)
}
}
// 作為一個構造函數注入類型的依賴
class ExampleServiceImpl @Inject constructor(
@AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
) : ...
// 需要被注入的類
@AndroidEntryPoint
class ExampleActivity: AppCompatActivity() {
@AuthInterceptorOkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient
}
系統提供的預定義限定符
Hilt 提供了一些預定義的限定符。例如,由于您可能需要來自應用或 Activity 的 Context 類,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
class AnalyticsAdapter @Inject constructor(
@ActivityContext private val context: Context,
private val service: AnalyticsService
) { ... }
為Android類生成的組件
對于系統提供的每一個Android類,都有一個關聯的Hilt組件,您可以再@InstallIn注解中引用該組件。每個Hilt組件負責將其綁定注入相應的Android類。
前面演示了如何再Hilt模塊中使用ActivityComponent。
Hilt提供了以下組件:
Hilt組件 | 注入器面向的對象 |
---|---|
ApplicationComponent | Application |
ActivityRetainedComponent | ViewModel |
ActivityComponent | Activity |
FragmentComponent | Fragment |
ViewComponent | View |
ViewWithFragmentComponent | 帶有WithFragmentBindings注解的View |
ServiceComponent | Service |
注意:Hilt不會為廣播接收器生成組件,因為Hilt直接從ApplicationComponent注入廣播接收器
組件作用域
默認情況下,Hilt中的所有綁定都未限定作用域 ->每當應用請求綁定時,Hilt都會創建所需類型的一個新實例。
不過,Hilt也允許將綁定的作用域限定為特定組件。Hilt只為綁定作用域限定到的組件的每個實例創建一次限定作用域的綁定,對該綁定的所有請求共享同一實例。
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會在相應Activity的真個生命周期內提供AnalyticsAdapter的同一實例:
@ActivityScoped
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
注意:將綁定的作用域限定為某個組件的作用域的成本可能很高,因為提供的對象在該組件被銷毀之前一直保留在內存中。所以,在應用中盡量少用限定作用域的綁定。如果綁定的內部狀態要求在某一作用域內使用同一實例,或者綁定的創建成本很高,那么將幫的作用域限定為某個組件是一種恰當的做法。
組件默認綁定
每個Hilt組件都附帶一組默認綁定,Hilt可以將其作為依賴項注入您自己的自定義綁定。請注意,這些綁定對應于常規Activity和Fragment類型,而不對應于任何特定子類。因為,Hilt會使用單個Activity組件定義來注入所有Activity。每個Activity都有此組件的不同實例。
Android組件 | 默認綁定 |
---|---|
ApplicationComponent | Application |
ActivityRetainedComponent | Application |
ActivityComponent | Application和Activity |
FragmentComponent | Application、 |
ViewComponent | View |
ViewWithFragmentComponent | 帶有WithFragmentBindings注解的View |
ServiceComponent | Service |
可以使用@ApplicationContext或@ActivityContext獲得應用或Activity的上下文綁定。
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext context: Context
) : AnalyticsService { ... }
// The Application binding is available without qualifiers.
class AnalyticsServiceImpl @Inject constructor(
application: Application
) : AnalyticsService { ... }