移動端開發(fā)之Jetpack?Hilt技術(shù)實現(xiàn)解耦
Hilt是什么
Hilt 是基于 Dagger2 的針對 Android場景定制化 的框架。
這有點像什么? RxAndroid 是 RxJava 的Android平臺定制化擴展。Andorid雖然由Java、Kotlin構(gòu)成,但是它有很多平臺的特性,比如它有 Java開發(fā) 所不知道的 Context 等。
Dagger框架雖然很出名,在國外也很流行,但是在國內(nèi)使用其的App少之又少,列舉一些缺點:
- 上手難,眾多Android應(yīng)用框架中,Dagger必定是最難學(xué)的那一檔;
- 它就是一個復(fù)雜框架,沒有針對任何平臺,所以對于所有平臺來說會難用;
- 在Android Studio4.0版本以前,無法追蹤Dagger的依賴關(guān)系(就類比IDE無法通過快捷鍵知道一個接口有哪些實現(xiàn)類) 開發(fā)者不知道為啥要做依賴注入
- 對于第三點,Android Studio4.1已經(jīng)支持了該功能,但是4.1有許多Bug,很多開發(fā)者都沒有升級 。
Hilt的出現(xiàn)解決前兩點問題,因為 Hilt 是 Dagger 針對Android平臺的場景化框架,比如Dagger需要我們手動聲明注入的地方,而Android聲明的地方不都在 onCreate()嗎,所以Hilt就幫我們做了,除此之外還做了很多事情,這樣一來,相較于Dagger,我們能用更少代碼、更容易、更輕松的配置依賴注入。
Hilt使用地方
Google認為移動端應(yīng)用的架構(gòu)設(shè)計,最重要的 Separation of concerns(分離關(guān)注點)。上網(wǎng)找解釋,其實它就是 模塊解耦。下面是Google官方推薦的Android應(yīng)用架構(gòu)圖:
依賴注入(DI)概念
而 Hilt 是被定義為 依賴注入框架而被發(fā)布。什么?又是依賴注入框架?不是之前已經(jīng)有了一個 Dagger2 了嗎?除了 Dagger2, 還有 ButterKnife ,Kotlin甚至還有輕量級的 Koin。
什么是依賴注入?先來看下面代碼:
class MyClass { val user = User() }
我們在一個類 MyClass 中 聲明了一個變量 user,并初始化-------調(diào)用 User 構(gòu)造函數(shù),創(chuàng)建一個對象。
上面這段代碼就產(chǎn)生了一個依賴關(guān)系。
我們要先看懂誰依賴了誰?首先 MyClass 是我們的類,User 可以是我們自己寫的類,也可以是通過第三方Jar包或SDK里面的類。 在我們寫的 MyClass 的代碼里面,我們需要一個 User 對象來完成一些任務(wù),所以我們創(chuàng)建了 User 對象,這就說明 MyClass 依賴了 User。對于 MyClass來說,User是外面之物,但是又需要依賴它。
如果上面這個 User,不是由自己創(chuàng)建,而是由外部創(chuàng)建,然后在本類只做賦值工作 ,這個過程就是 依賴注入。
有一個我們非常熟悉的設(shè)計模式,就使用了依賴注入的方法—工廠模式:
class UserFactory { fun newUser(): User{ return User() } } class MyClass { val user = UserFactory.newUser() }
我們的 MyClass 類需要使用 User 類,但是這次沒有自己來創(chuàng)建(沒有自己new出來),而是交由給 UserFactory 來創(chuàng)建出來,MyClass就做了最后的賦值工作。
對于 MyClass 來說,這就是一次依賴注入,和上面例子相比,把對象創(chuàng)建的過程交由給了別的類。
所以我們通過上面兩個例子就能知道依賴注入的本質(zhì)是什么:借由外部得到對象。依賴注入框架就是這個外部
現(xiàn)在流行的 Dagger2、Koin框架,只是讓我們更輕松、更容易的去得到對象。
Dagger的中文翻譯是 “刀”,它就像一把刀,插進我們代碼中。那相信你也知道 ButterKnife 為什么這么取名了吧。
Hilt使用
導(dǎo)入
在 app 的 build.gradle 中加入:
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
...
implementation 'com.google.dagger:hilt-android:2.28-alpha'
kapt 'com.google.dagger:hilt-android-compiler:2.28-alpha'
}
在 project的 build.gradle 中加入:
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
一個簡單的例子 Hilt 需要 AndroidManifest 使用帶有 @HiltAndroidApp 注解的 Application 類,所以我們的 Application需要這樣:
@HiltAndroidApp class HiltApp : Application() { ... }
然后在 AndroidManifest 文件中聲明:
<application android:name=".HiltApp" ... </application>
假如我們要在 MainActivity 中注入一個 User 對象, 我們首先編寫一個 User 類,User類有兩個屬性 name 和 age 。 誒,這個時候有同學(xué)就會問了:我通過 @Inject 聲明一個User,Hilt就能給我創(chuàng)建一個 User 對象,那這個User對象里面的name和age是啥?答案是:編譯會報錯,因為我們自己都不知道這兩個參數(shù)是啥,Hilt怎么可能會知道,所以這兩個屬性也要通過依賴注入的方式來注入。
為了簡化這個問題,我定義一個默認的無參構(gòu)造函數(shù),反正創(chuàng)建之后,里面的值也是可以修改的嘛。
data class User(var name: String, var age: Int) { // 定義一個默認的無參構(gòu)造函數(shù),并使用 @Inject 注解修飾 @Inject constructor() : this("Rikka", 23) }
接著我們在 MainActivity 中聲明一個 User,通過 @Inject 來修飾,并且MainActivity 需要通過 @AndroidEntryPoint 修飾:
// 1 @AndroidEntryPoint class MainActivity : AppCompatActivity() { // 2 @Inject lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d(TAG, "user name:${user.name} age:${user.age}") } }
Logcat 打印結(jié)果如下:
代碼解析:
注釋1: 為 MainActivity 修飾 @AndroidEntryPoint,該注解表明 該類為需要進行依賴注入的 Android類,是Dagger針對Android場景化的地方。當我們類中需要進行依賴注入,我們?yōu)樵擃惣尤脒@個注解,它會幫助創(chuàng)建一個單獨的 Hilt組件。它不能修飾Abstract,它只能修飾:
- ComponentActivity
- (Support)Fragment
- View
- Service
- BroadcastReceiver
注釋2:我們需要注入一個 User,所以我們給它加一個 @Inject 注解,告訴 Hilt。因為Kotlin的語法問題,這里不得不聲明 lateinit(而這里Koin的寫法更加優(yōu)雅),接下來步驟大概是這樣的:
- Hilt 會去找User 這個類的構(gòu)造函數(shù),以此來創(chuàng)建一個對象
- Hilt 發(fā)現(xiàn) 有兩個個構(gòu)造函數(shù),而無參構(gòu)造函數(shù)被@Inject 聲明
- Hilt 會去調(diào)用被@Inject 的構(gòu)造函數(shù),創(chuàng)建一個User(“Rikka”, 23) 對象
- 返回這個對象, MainActivity 實現(xiàn)外部幫忙創(chuàng)建 User對象,實現(xiàn) User 的依賴注入。
Inject 的中文翻譯是 “注入、注射”,所以可以形象的認為, @Inject 修飾的變量是被外界通過針筒注入進來的。
- @Inject 可以修飾
- 構(gòu)造函數(shù) Constructors
- 變量 Fields
- 方法 Methods
構(gòu)造函數(shù)是最先被注解的,然后再是變量和方法。所以它修飾構(gòu)造函數(shù)和修飾變量,其實是不同的作用。但為了便于理解,我們可以把它看成是一個插眼工具,便于Hilt去尋找要注入的地方。
我們上面的 User 類是無參構(gòu)造函數(shù),這次假設(shè)我們要有參數(shù)的呢?其實就是參數(shù)也要注入嘛,這就是套娃來的,來看看我們給User新增一個 屬性:Clothes:
class Clothes @Inject constructor() { } class User @Inject constructor(var clothes: Clothes){ } @AndroidEntryPoint class MainActivity : AppCompatActivity() { ... Log.d(TAG, "user clothes:${user.clothes}") }
打印結(jié)果:
PS:大家不要太拘泥于有參構(gòu)造函數(shù)的創(chuàng)建,我認為注入的作用是創(chuàng)建出一個對象,這個對象里面的內(nèi)容可以后續(xù)再傳入,它更多的體現(xiàn)、或者我們需要注意的是 “分離關(guān)注點”
實現(xiàn)接口實例注入
因為接口沒有構(gòu)造函數(shù),所以當我們想要依賴一些接口時,該怎么辦。
我們來下面的示例,我們寫一個 Profession 接口,代表職業(yè):
interface Profession { fun doJob() }
假設(shè)我們有兩個實現(xiàn)接口的類:醫(yī)生類和程序猿類:
class Doctor : Profession{ override fun doJob() { Log.d("Doctor", "doctor do job") } } class Programmer : Profession{ override fun doJob() { Log.d("Programmer", "programmer do job") } }
這個時候我給 User 類添加一個職業(yè)的屬性,并希望它能夠自動注入:
class User @Inject constructor(var clothes: Clothes){ @Inject lateinit var profession: Profession }
因為 Profession 是一個接口,它有兩個實現(xiàn)類,所以這樣 Hilt 并不能知道我們要實例化哪個具體的實現(xiàn)類,所以編譯的時候就會報錯。
而 Hilt 也解決這種問題,首先我們要在每個實現(xiàn)類上注入構(gòu)造函數(shù):
class Doctor @Inject constructor() : Profession{ ... } class Programmer @Inject constructor() : Profession{ ... }
接著我們需要實現(xiàn)一個和該接口有關(guān)的 XXXModule類,它被 @Module 修飾,這個和 Dagger 中的一樣,代表它會為接口提供一個創(chuàng)建實例的工廠,同時需要加上 @InstallIn 注解,用來聲明它是被安裝到哪個組件中,該注解后面會說到。 代碼如下:
@Module @InstallIn(ActivityComponent::class) abstract class ProfessionModule { // 1 // 2 @Binds abstract fun bindDoctor(doctor: Doctor): Profession }
注釋1: 我們寫出來的類是一個抽象類,因為我們不需要具體的實現(xiàn)它,而且它沒有具體的命名規(guī)則,因為我們也不會在代碼中直接調(diào)用它,但是為了便于理解,我們起名一般叫 接口名 + Module。
注釋2: 我們假設(shè)該Module提供一個 Doctor 的職業(yè),那我們需要定義一個 抽象方法 來獲取一個Doctor類。 并且 該方法需要被 @Binds 注解修飾。這樣就能被 Hilt 識別。
這樣一來,我們就實現(xiàn)了接口的一個實例化的注入,我們來實驗一下,在 User 中去展示它:
class User @Inject constructor(var clothes: Clothes){ @Inject lateinit var profession: Profession fun showMyself() { profession.doJob() } } // MainActivity @AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) user.showMyself() } }
打印結(jié)果為:
可以看到我們的 Doctor 成功的注入了。
OK,我們了解了接口某一個實現(xiàn)類的注入 (Doctor),那假設(shè)這個時候,另外一個類需要注入接口的另一個實現(xiàn)類 Programmer,那我們是不是也得按照同樣的做法,在 Module類中添加呢?
@Module @InstallIn(ActivityComponent::class) abstract class ProfessionModule { @Binds abstract fun bindDoctor(doctor: Doctor): Profession @Binds abstract fun bindProgrammer(programmer: Programmer): Profession }
這個時候發(fā)現(xiàn)運行,編譯也會報錯:
提示我們被綁定多次了。
這是因為 Doctor 和 Programmer 都是相同類型,當他們一起被 Binds 注解,那 Hilt 不知道要去綁定哪一個。
這個時候就需要使用 @Qualifier 注解來幫助我們了,它就是為了這種 相同類型 依賴注入而產(chǎn)生的:
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class BindDoctor @Qualifier @Retention(AnnotationRetention.BINARY) annotation class
我們創(chuàng)建了新的注解 BindDoctor 和 BindProgrammer,他們都被 @Qualifier 修飾,表示他們用來作用在同種類型上, @Retention 選擇使用 BINARY類型,表明該注解保留到編譯后,但無法通過反射來得到,是比較適合的注解。
接下來,我們要將這些注解作用在被 Binds 注解的抽象方法上:
@Module @InstallIn(ActivityComponent::class) abstract class ProfessionModule { @BindDoctor @Binds abstract fun bindDoctor(doctor: Doctor): Profession @BindProgrammer @Binds abstract fun bindProgrammer(programmer: Programmer): Profession }
最后,在 User 中聲明使用哪一個類型的注入:
class User @Inject constructor(var clothes: Clothes){ @BindProgrammer // 這次注入一個 Programmer @Inject lateinit var profession: Profession fun showMyself() { profession.doJob() } }
打印結(jié)果如下所示:
這下我們就實現(xiàn)了具體某個實例的注入啦。
實現(xiàn)第三方依賴注入
假設(shè)一些類不是由我們自己寫的,而是由第三方庫導(dǎo)入的。比如 OkHttp ,我們在使用網(wǎng)絡(luò)請求的時候,需要使用它,為了分離關(guān)注點,我們需要對他進行依賴注入。
但是 OkHttp 是我們不能修改的類,所以我們不能在它的構(gòu)造函數(shù)上加入 @Inject, 這個時候該怎么辦呢?
Dagger 中也有類似的場景,我們需要 @Providers 來幫助我們。除此之外,我們也需要 @Module 注解來聲明一個 Module 類, 基于上面的例子,我們可以把這種 Module 類看成是一個工廠:
@Module @InstallIn(ApplicationComponent::class) class NetModule { // 1 // 2 @Provides fun provideOkHttpClient(): OkHttpClient { // 3 return OkHttpClient.Builder().build() } }
注釋1: 聲明一個 NetModule,提供網(wǎng)絡(luò)庫相關(guān)的組件,并沒有和上面例子一樣聲明為抽象函數(shù),這是因為里面的都有具體的實現(xiàn)方法。
注釋2: 編寫一個 provideOkHttpClient() 方法,返回一個 OkHttpClient對象。 聲明一個 @Providers 注解,表示這個提供的依賴對象,是第三方的類或者系統(tǒng)類,我們因為不能直接更改其構(gòu)造函數(shù),所以得加上這個注解。
注釋3:new 一個對象,并返回。
這樣,我們就能在我們代碼中直接使用了:
@Inject lateinit var okHttpClient:
@Providers 的本質(zhì)是什么? 第三方類因為其只讀性,Hilt不能找到其構(gòu)造函數(shù),所以需要我們自己手動的創(chuàng)建,創(chuàng)建的方法被 @Providers 修飾, Hilt 找到這個方法,并提供由我們手動創(chuàng)建的對象。
所以 @Providers 的本質(zhì),是由我們自己創(chuàng)建對象, Hilt 幫我們注入。
現(xiàn)在大家都不會直接使用 OkHttp,而是使用 Retrofit,所以我們來提供一個 Retrofit 把:
... @Provides fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() }
因為 Retrofit的創(chuàng)建需要依賴一個 OkHttpClient 對象,所以我們需要創(chuàng)建一個,但是我們也可以注入一個,因為我們之前已經(jīng)有 provideOkHttpClient,所以它就能提供一個實例,我們不用在擔心什么了。
Hilt 的內(nèi)置組件和作用域
@InstallIn 注解
我們之前看到了 @InstallIn 這個注解,它的作用是用來表明 Module 作用的地方,它的參數(shù)時 xxxComponent格式,前面xx代表作用域。
因為 Hilt 是Dagger的Android場景化,所以它能作用的地方和我們Android息息相關(guān),有下面幾處:
- Application ->ApplicationComponent
- ViewModel ->ActivityRetainedComponent
- Activity ->ActivityComponent
- Fragment ->FragmentComponent
- View ->ViewComponent
- Service ->ServiceComponent
- View Annotation with@WithFragmentBindings ->ViewWithFragemntComponent
除了最后一個,別的作用域還是挺常見的。他們都要通過 @InstallIn 注入。
比如我們ProfessionModule是聲明成 @InstallIn(ActivityComponent::class),這就說明只有在 Activity 的代碼中可以使用它,而在 Service 中是不能使用的。 而 NetModule 則是聲明成 InstallIn(ApplicationComponent::class)的,這說明在全局都可以使用它提供的 OkHttpClient 和 Retrofit 對象。
使注入對象單例
像 Retrofit 、 OkHttpClient 這樣的全局都需要使用到的對象,我們希望它的作用域是全局,并且單例的。
但是 Hilt 提供的 @Inject 對象并不是單例的,每次 注入時都會重新生成一個新的實例,這就說明,假設(shè)我們要使用 Retrofit 來做網(wǎng)絡(luò)請求, @Providers 每次提供的都是不一樣的,這樣對性能來說很不友好,而且不符合常規(guī)的邏輯設(shè)定。
按照以往,我們可以通過給 NetModule 聲明一個 @Singleton 注解,來讓這個類實現(xiàn)單例,來解決這個問題。
Hilt 也有自己的解決方案,那就是使用 @xxxScope 注解,它和上面的 xxxComponent所對應(yīng),表示 在這個作用域內(nèi)單例,來看看對應(yīng)關(guān)系:
- Application ->ApplicationComponent ->@Singleton
- ViewModel ->ActivityRetainedComponent ->@ActivityRetainedScoped
- Activity ->ActivityComponent ->@ActivityScoped
- Fragment ->FragmentComponent ->@FragmentScoped
- View ->ViewComponent ->@ViewScoped
- Service ->ServiceComponent ->@ServiceScoped
- View Annotation with@WithFragmentBindings ->ViewWithFragemntComponent ->@ViewScoped
使用 @xxScoped 來替代 @InstallIn(xxxComponent::class) 聲明組件為單例。
因為 Application 是作用于全局,所以它的注解是 @Singleton,比較好理解。
作用域的包含關(guān)系
作用域也有自己的包含關(guān)系,比如被 @ActivityScoped聲明的組件,可以在 Fragment 或者 View 中使用,他們的具體包含關(guān)系如下圖所示:
Hilt 預(yù)置的 Qualifier
我介紹過 Hilt 是 Dagger針對Android的場景化,所以它低層做了很多事情,使得在Android上更好的使用。除了上面介紹過的那些注解外,還有很多別的東西,可以讓我們?nèi)ヌ剿?,同時也了解了Dagger本身。
Context 上下文是Android 獨特的存在,它代表著 Application、Activity、Service的一些fwk層的東西。
而我們的代碼中經(jīng)常會需要 Context 來創(chuàng)建一些東西:
class A @Inject constructor(context: Context) { ... }
但是我們知道,它是系統(tǒng)類,我們無法注入 Context。那我們可以通過使用 @Providers 來創(chuàng)建嗎?
@Providers fun provideContext() { ??? }
很明顯,Context 是由AMS來創(chuàng)建的,我們無法直接創(chuàng)建一個上下文出來。這個問題該如何解決呢?
答案是:我們不用解決,Hilt 為我們提供了它自己預(yù)置的注解 @ApplicationContext 和 @ActivityContext,我們直接使用,Hilt會幫我們注入上下文。
class A @Inject constructor(@ApplicaitonContext context: Context)
而現(xiàn)在沒有 ServiceContext,可能是用的比較少吧?
@ApplicationContext 提供的類型是 Application, 而不是我們自己的 App 自定義的 Application,加入我們要使用自己的該怎么辦呢?答案是也很簡單:
@Providers fun provideApplicaiton(application: Application): MyApplication { return applicaiton as MyApplication }
直接在 Module 中提供一個,并強轉(zhuǎn)就 OK啦。
注意
ApplicationContext 的作用域是全局, 所以它修飾的類的作用只能是 @InstallIn(Applicaiton) 或 @Singleton,其他的也同理。
之前沒有用過 Dagger,因為項目不需要,且難學(xué),問題多。 Hilt 出來之后解決了大部分的痛點,再不上車屬實就有點說不過去了。
Hilt 相較與 Dagger,肯定是更好用,更適合Android來使用。 它和 Koin的比較,只是性能上的差異,網(wǎng)上大部分的文章都認為 Hilt 性能更優(yōu),但是代碼量更多,在大的項目使用 Hilt 會更好,而小的項目兩者差別不會太大。具體還請開發(fā)者自己研究。
Hilt 作用是 提供依賴注入,幫助程序分離關(guān)注點,幫助搭建低耦合高內(nèi)聚的框架,學(xué)習(xí)它,有利于我們學(xué)習(xí) Android應(yīng)用架構(gòu) 方面的技能。
到此這篇關(guān)于移動端開發(fā)之Jetpack Hilt技術(shù)實現(xiàn)解耦的文章就介紹到這了,更多相關(guān)Jetpack Hilt解耦內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android實現(xiàn)文字翻轉(zhuǎn)動畫的效果
本文實現(xiàn)了Android程序文字翻轉(zhuǎn)動畫的實現(xiàn),具有一定的參考價值,有需要的朋友可以了解一下。2016-10-10Android網(wǎng)絡(luò)開發(fā)中GET與POST請求詳解
這篇文章主要介紹了android實現(xiàn)網(wǎng)絡(luò)請求的get和post請求的簡單封裝與使用,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧<BR>2022-12-12viewPager+fragment刷新緩存fragment的方法
這篇文章主要介紹了viewPager+fragment刷新緩存fragment的方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-03-03詳解Android Studio中Git的配置及協(xié)同開發(fā)
這篇文章主要介紹了詳解Android Studio中Git的配置及協(xié)同開發(fā),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03Android開發(fā)之使用SQLite存儲數(shù)據(jù)的方法分析
這篇文章主要介紹了Android開發(fā)之使用SQLite存儲數(shù)據(jù)的方法,結(jié)合實例形式分析了Android使用SQLite數(shù)據(jù)庫實現(xiàn)針對數(shù)據(jù)的增刪改查操作相關(guān)技巧,需要的朋友可以參考下2017-07-07