在元件化AwesomeGithub專案中使用了Dagger
來減少手動依賴注入程式碼。雖然它能自動化幫我們管理依賴項,但是寫過之後的應該都會體會到它還是有點繁瑣的。專案中到處充斥著Component
,這讓我想起了傳統MVP
模式的介面定義。
簡單來說就是費勁,有許多大量的類似定義。可能google
也意識到這一點了,所以前不久釋出出了Hilt
。
Hilt
為了防止沒聽說過的小夥伴們一頭霧水,首先我們來了解下Hilt
是什麼?
Hilt
是Android
的依賴注入庫,可減少在專案中執行手動依賴項注入的樣板程式碼。
Hilt
通過為專案中的每個 Android
類提供容器並自動管理其生命週期,提供了一種在應用中使用 DI
(依賴項注入)的標準方法。Hilt
在Dagger
的基礎上構建而成,因而能夠具有 Dagger
的編譯時正確性、執行時效能、可伸縮性。
那麼有的小夥伴可能會有疑問,既然已經有了Dagger
那為什麼還要Hilt
的呢?
Hilt
與Dagger
的主要目標都是一致的:
- 簡化
Android
應用的Dagger
相關基礎架構。 - 建立一組標準的元件和作用域,以簡化設定、提高可讀性以及在應用之間共享程式碼。
- 提供一種簡單的方法來為各種構建型別(如測試、除錯或釋出)配置不同的繫結。
但是Android
中會例項化許多元件類,例如Activity
,因此在應用中使用Dagger
需要開發者編寫大量的樣板程式碼。Hilt
可以減少這些樣板程式碼。
Hilt
做的優化包括
- 無需編寫大量的
Component
程式碼 -
Scope
也會與Component
自動繫結 - 預定義繫結,例如
Application
與Activity
- 預定義的限定符,例如
@ApplicationContext
與@ActivityContext
下面通過AwesomeGithub中Dagger
來對比了解Hilt
的具體使用。
依賴
使用之前將Hilt
的依賴新增到專案中。
首先,將hilt-android-gradle-plugin
外掛新增到專案的根級 build.gradle
檔案中:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
}
}
然後,應用Gradle
外掛並在app/build.gradle
檔案中新增以下依賴項:
...
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類
使用Dagger
時,需要一個AppComponent
單例元件,專案中的其它SubComponent
都將依賴於它,所以在AwesomeGithub中它大概是這個樣子
@Singleton
@Component(
modules = [
SubComponentModule::class,
NetworkModule::class,
ViewModelBuilderModule::class
]
)
interface AppComponent {
@Component.Factory
interface Factory {
fun create(@BindsInstance applicationContext: Context): AppComponent
}
fun welcomeComponent(): WelcomeComponent.Factory
fun mainComponent(): MainComponent.Factory
...
fun loginComponent(): LoginComponent.Factory
}
@Module(
subcomponents = [
WelcomeComponent::class,
MainComponent::class,
...
LoginComponent::class
]
)
object SubComponentModule
上面的我已經省略了大半,是不是看起來很多,而且最重要的是很多重複的結構基本都是一樣的。所以Hilt
基於這一點進行了簡化,將這些重複的編寫轉成構建的時候自動生成。
Hilt
要做的很簡單,新增幾個註釋
@HiltAndroidApp
class App : Application() { ... }
所有的Hilt
應用都必須包含一個帶@HiltAndroidApp
註釋的Application
。它將替代Dagger
中的AppComponent
。
Android類
對於Android
類,使用Dagger
時需要定義SubComponent
並將它依賴到Application
類中。下面以WelcomeActivity
為例。
@Subcomponent(modules = [WelcomeModule::class])
interface WelcomeComponent {
@Subcomponent.Factory
interface Factory {
fun create(): WelcomeComponent
}
fun inject(activity: WelcomeActivity)
}
module的部分先不說,後面會提及
下面看Hilt
的實現
@AndroidEntryPoint
class MainActivity : BaseHiltActivity<ActivityMainBinding, MainVM>() { ... }
Hilt
要做的是新增@AndroidEntryPoint
註釋即可。
驚訝,結合上面的,兩個註解就替換了Dagger
的實現,現在是否體會到Hilt
的簡潔?對新手來說也可以降低很大的學習成本。
目前Hilt
支援下面Android
類
- Application (@HiltAndroidApp)
- Activity
- Fragment
- View
- Searvice
- BroadcastReceiver
有一點需要注意,如果使用@AndroidEntryPoint
註釋了某個類,那麼依賴該類的其它類也需要新增。
典型的就是Fragment
,所以除了Fragment
還需要給依賴它的所有Activity
進行註釋。
@AndroidEntryPoint
的作用,對照一下Dagger
就知道了。它會自動幫我們生成對應Android
類的Componennt
,並將其新增到Application
類中。
@Inject
@Inject
的使用基本與Dagger
一致,可以用來定義構造方法或者欄位,宣告該構造方法或者欄位需要通過依賴獲取。
class UserRepository @Inject constructor(
private val service: GithubService
) : BaseRepository() { ... }
@Module
Hilt
模組也需要新增@Module
註釋,與Dagger
不同的是它還必須使用@InstallIn
為模組新增註釋。目的是告知模組用在哪個Android
類中。
@Binds
@Binds
註釋會告知Hilt
在需要提供介面的例項時要使用哪種實現。
它的用法與Dagger
沒什麼區別
@Module
@InstallIn(ActivityComponent::class)
abstract class WelcomeModule {
@Binds
@IntoMap
@ViewModelKey(WelcomeVM::class)
abstract fun bindViewModel(viewModel: WelcomeVM): ViewModel
}
不同的是需要新增@InstallIn
,ActivityComponent::class
用來表明該模組作用範圍為Activity
其實上面這塊對ViewModel
的注入,使用Hilt
時會自動幫我們編寫,這裡只是為了展示與Dagger
的不同之處。後續會提到ViewModel
的注入。
@Providers
提供一個FragmentManager
的例項,首先是Dagger
的使用
@Module
class MainProviderModule(private val activity: FragmentActivity) {
@Provides
fun providersFragmentManager(): FragmentManager = activity.supportFragmentManager
}
對比一下Hilt
@InstallIn(ActivityComponent::class)
@Module
object MainProviderModule {
@Provides
fun providerFragmentManager(@ActivityContext context: Context) = (context as FragmentActivity).supportFragmentManager
}
區別是在Hilt
中@Providers
必須為static
類並且構造方法不能有引數。
@ActivityContext
是Hilt
提供的預定限定符,它能提供來自與Activity
的Context
,對應的還有@ApplicationContext
提供的元件
對於之前提到的@InstallIn
會關聯不同的Android
類,除了@ActivityComponent
還有以下幾種
對應的生命週期如下
同時還提供了相應的作用域
所以Hilt
的預設提供將大幅提高開發效率,減少許多重複的定義
ViewModel
最後再來說下ViewModel
的注入。如果你使用到了Jetpack
相信少不了它的注入。
對於Dagger
我們需要自定義一個ViewModelFactory
,並且提供注入方式,例如在AwesomeGithub的componentbridget
模組中定義了ViewModelFactory
@Module
abstract class ViewModelBuilderModule {
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
class ViewModelFactory @Inject constructor(private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var creator = creators[modelClass]
if (creator == null) {
for ((key, value) in creators) {
if (modelClass.isAssignableFrom(key)) {
creator = value
}
}
}
if (creator == null) {
throw IllegalArgumentException("Unknown model class: $modelClass")
}
try {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException()
}
}
}
通過@Inject
來注入構造例項,但構造方法中需要提供Map
型別的creators
。這個時候可以使用@IntoMap
,為了匹配Map
的型別,需要定義一個@MapKey
的註釋
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
然後再到對應的元件下使用,例如匹配MainVM
@Module
abstract class MainModule {
@Binds
@IntoMap
@ViewModelKey(MainVM::class)
abstract fun bindViewModel(viewModel: MainVM): ViewModel
}
這樣就提供了Map<Class<MainVM>, MainVM>
的引數型別,這時我們自定義的ViewModelFactory
就能夠被成功注入。
例如basic
模組裡面的BaseDaggerActivity
abstract class BaseDaggerActivity<V : ViewDataBinding, M : BaseVM> : AppCompatActivity() {
protected lateinit var viewDataBinding: V
@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
protected val viewModel by lazy { ViewModelProvider(this, viewModelFactory)[getViewModelClass()] }
...
}
當然,別忘了MainVM
也需要使用@Inject
來宣告注入
class MainVM @Inject constructor() : BaseVM() { ... }
以上是Dagger
的ViewModel
使用的注入方式。
雖然自定義的ViewModelFactory
是公用的,但是對於不同的ViewModel
還是要手動定義不同的bindViewModel
方法。
而對於Hilt
卻可以省略這一步,甚至說上面的全部都不需要手動編寫。我們需要做的是隻需在ViewModel
的建構函式上新增@ViewModelInject
。
例如上面的MainVM
,使用Hilt
的效果如下
class MainVM @ViewModelInject constructor() : BaseVM() { ... }
至於Hilt
為什麼會這麼簡單呢?我們不要忘了它的本質,它是在Dagger
之上建立的,本質是為了幫助我們減少不必要的樣板模板,方便開發者更好的使用依賴注入。
在Hilt
中,上面的實現會自動幫我們生成,所以才會使用起來這麼簡單。
如果你去對比看AwesomeGithub上的feat_dagger與feat_hilt兩個分支中的程式碼,就會發現使用Hilt
明顯少了許多程式碼。對於簡單的Android
類來說就是增加幾個註釋而已。
目前唯一一個比較不理想的是對於@Providers
的使用,構造方法中不能有引數,如果在用Dagger
使用時已經有引數了,再轉變成Hilt
可能不會那麼容易。
慶幸的是,Dagger
與Hilt
可以共存。所以你可以選擇性的使用。
但是整體而言Hilt
真香,你只要嘗試了絕不會後悔~
AwesomeGithub
AwesomeGithub是基於Github的客戶端,純練習專案,支援元件化開發,支援賬戶密碼與認證登陸。使用Kotlin語言進行開發,專案架構是基於JetPack&DataBinding的MVVM;專案中使用了Arouter、Retrofit、Coroutine、Glide、Dagger與Hilt等流行開源技術。
除了Android原生版本,還有基於Flutter的跨平臺版本flutter_github。
如果你喜歡我的文章模式,或者對我接下來的文章感興趣,你可以關注我的微信公眾號:【Android補給站】
或者掃描下方二維碼,與我建立有效的溝通,同時能夠更方便的收到相關的技術推送。