Hilt 是基於 Dagger 且設計在 Android 上使用的 dependency injection library。所以在開發 Android 時,使用 Hilt 會比使用 Dagger 更加地方便。本文章將介紹如何使用 Hilt。
Table of Contents
Hilt
Hilt 是基於 Dagger。如果你想了解 Dagger,可以參考以下文章。
在開始使用 Hilt 之前,我們必須要先加入以下的 dependencies。首先,在 project level 的 build.gradle 裡加上 hilt-android-gradle-plugin。
plugins {
id 'com.google.dagger.hilt.android' version '2.44' apply false
}在 module level 的 app/build.gradle 裡加上以下的 Gradle plugin 和 dependencies。
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
def hilt_version = "2.44"
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
}
// Allow references to generated code
kapt {
correctErrorTypes true
}Product List 範例
本文章會藉由一個 product list 範例來介紹如何使用 Hilt。在範例中,我們會使用 Room database 來儲存 product 資料。如果你不熟悉 Room database 的話,請先參考以下文章。
以下是 product 資料庫的程式碼。
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "products")
data class Product(
val name: String,
val price: Int,
@PrimaryKey(autoGenerate = true) var id: Int? = null
)import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface ProductDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: Product)
@Query("SELECT * FROM products")
suspend fun findAll(): List<Product>
}import androidx.room.Database
import androidx.room.RoomDatabase
@Database(entities = [Product::class], version = 1)
abstract class ProductDatabase : RoomDatabase() {
abstract fun dao(): ProductDao
}@Inject:注入依賴
@Inject 告訴 Hilt 在這邊注入需要的依賴。
以下程式碼中,我們在 ProductRepositoryImpl 的 constructor 前面加上 @Inject。所以,當 Hilt 在 instantiate ProductRepositoryImpl 時,會自動傳入 ProductDao。
在 constructor 前面加上 @Inject,這叫做 constructor injection。而在 filed 前面加上 @Inject 的話,就叫做 filed injection。
interface ProductRepository {
suspend fun addOrder(product: Product)
suspend fun getAllOrders(): List<Product>
}import javax.inject.Inject
class ProductRepositoryImpl @Inject constructor(private val dao: ProductDao) : ProductRepository {
override suspend fun addOrder(product: Product) {
dao.insert(product)
}
override suspend fun getAllOrders(): List<Product> {
return dao.findAll()
}
}@Module:Hilt Modules
剛剛我們介紹了使用 @Inject 來要求 Hilt 注入依賴。那問題是 Hilt 如何知道要怎麼 instantiate 那些依賴呢?我們必須用 Hilt modules 來提供那些 instances。
一個 Hilt module 是一個有加上 @Module 的 class。在一個 Hilt module 裡,我們需要定義有加上 @Provides 或 @Binds 的 functions 來提供 instances。
另外,在一個 Hilt module 上,我們也會加上 @InstallIn 來指定這個 Hilt module 會提供 instances 到哪個 Hilt component。
@Provides:提供 Instances
宣告 object AppModuleObject,並且加上 @Module。有加上 @Module 的 class 就是一個 Hilt module。Hilt module 告訴 Hilt 要如何提供某些 types 的 instances。
在 AppModuleObject 中,我們宣告兩個 functions,並且加上 @Provides。@Provides 告訴 Hilt,此 function 會提供一個 instance。此外,@Provides 還會提供以下資訊給 Hilt。
- Return type:此 function 要提供的 instance 的 type。
- Paratemeters:在呼叫此 function 時,Hilt 需要傳入的依賴。
- Body:告訴 Hilt 如何提供 return type 的 instance。實際上,Hilt 會執行此 function 來取得 instance。
所以,AppModuleObject 告訴 Hilt 如何提供 ProductDatabase 和 ProductDao 的 instances。
import android.content.Context
import androidx.room.Room
import com.waynestalk.hiltexample.product.ProductDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModuleObject {
@Singleton
@Provides
fun provideProductDatabase(@ApplicationContext appContext: Context): ProductDatabase {
return Room.databaseBuilder(appContext, ProductDatabase::class.java, "orders").build()
}
@Singleton
@Provides
fun provideProductDao(database: ProductDatabase): ProductDao {
return database.dao()
}
}@Binds:提供 Interface 的 Instances
剛剛我們宣告了 interface ProductRepository 和 class ProductRepositoryImpl。當我們在程式中,要求 Hilt 注入 ProductRepository 時,Hilt 要如何知道它要注入的是 ProductRepositoryImpl 呢?這時候,我們要用 @Binds。
在以下程式碼中,我們宣告 abstract class AppModuleClass,並且加上 @Module。所以,AppModuleClass 也是一個 Hilt module。
在 AppModuleClass 中,我們宣告一個 function,並且加上 @Binds。@Binds 告訴 Hilt,此 function 會提供一個 instance。此外,@Binds 還會提供以下資訊給 Hilt。
- Return type:此 function 要提供的 instance 的 type。
- Parameter:告訴 Hilt 是提供哪個 implementation。
import com.waynestalk.hiltexample.product.ProductRepository
import com.waynestalk.hiltexample.product.ProductRepositoryImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class AppModuleClass {
@Binds
abstract fun provideOrderRepository(impl: ProductRepositoryImpl): ProductRepository
}注入依賴到 Hilt 有支援的 Android Classes
當你在一個 Android class(如 Activity 和 Fragment)加上 @AndroidEntryPoint 後,你就可以在那個 Android class 裡面使用 @Inject 來要求 Hilt 來執行 field injection。@AndroidEntryPoint 會對每一個 Android class 產生一個 Hilt component。然後,Hilt 會從這些 Hilt components 中取得 instances 來注入依賴。
目前 Hilt 支援的 Android classes 如下:
- Application:使用 @HiltAndroidApp
- ViewModel:使用 @HiltViewModel
- Activity、Fragment、View、Service、BroadcastReceiver:使用 @AndroidEntryPoint
@HiltAndroidApp:注入依賴到 Android Application
所有使用 Hilt 的 apps 必須要在 Application class 加上 @HiltAndroidApp。
@HiltAndroidApp 會啟動 Hilt 的 code generation。
import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class AppApplication : Application()
@HiltViewModel:注入依賴到 ViewModel
如下程式碼中,我們對 ProductListViewModel 加上 @HiltViewModel。所以,Hilt 會對 ProductListViewModel 產生一個個別的 Hilt component。然後,從這 Hilt component 中取得 ProductRepository 的 instance,並將它注入 constructor。
@HiltViewModel
class ProductListViewModel @Inject constructor(private val productRepository: ProductRepository) :
ViewModel() {
val orders: MutableLiveData<List<Product>> = MutableLiveData()
val addResult: MutableLiveData<Result<Unit>> = MutableLiveData()
fun getAllOrders() {
viewModelScope.launch(Dispatchers.IO) {
val list = productRepository.getAllOrders()
orders.postValue(list)
}
}
fun addProduct(product: Product) {
viewModelScope.launch(Dispatchers.IO) {
productRepository.addOrder(product)
addResult.postValue(Result.success(Unit))
}
}
}@AndroidEntryPoint:注入依賴到其他 Android Classes
以下程式碼中,我們在 ProductListFragment 中,要求 Hilt 注入 ProductListAdapter。所以,我們必須要在 ProductListFragment 上加上 @AndroidEntryPoint。因為 ProductListActivity 依賴於 ProductListFragment,所以當 ProductListFragment 加上 @AndroidEntryPoint 時,ProductListActivity 也必須要加上 @AndroidEntryPoint。
@AndroidEntryPoint
class ProductListActivity : AppCompatActivity() {
}@AndroidEntryPoint
class ProductListFragment : Fragment() {
@Inject
lateinit var adapter: ProductListAdapter
private val viewModel: ProductListViewModel by viewModels()
}class ProductListAdapter @Inject constructor() :
RecyclerView.Adapter<ProductListAdapter.ViewHolder>() {
}@EntryPoint:注入依賴到 Hilt 沒有支援的 Classes
Hilt 支援大部分常用的 Android classes。當你在一個 Hilt 有支援的 Android class 上加上 @AndroidEntryPoint 時,它會對這個 Android class 產生一個 Hilt component。然後,Hilt 會從這個 Hilt component 中取得 instance,然後對有加上 @Inject 的 filed 執行 field injection。
那如果我們需要在 Hilt 沒有支援的 classes 裡,要求 Hilt 對有加上 @Inject 的 filed 執行 field injection 時,那我們要如何做呢。實際上,我們無法做到。
在 Hilt 沒有支援的 classes 裡,我們不能用 @Inject 來要求 Hilt 執行 field injection。不過,我們還是可以用 @EntryPoint 和 @InstallIn 來從 Hilt component 中取得 instances。
例如,Hilt 沒有支援 ContentProvider,而我們想要在 ContentProvider 裡取得 ProductRepository。這時我們可以宣告 interface ProductsContentProviderEntryPoint,並在裡面宣告一個 function 且其 return type 為 ProductRepository。在 ProductsContentProviderEntryPoint 上加上 @EntryPoint。我們還需要加上 @InstallIn 來告訴 Hilt 是要在 SingletonComponent 裡建立一個 entry point。所以當 Hilt 在產生 SingletonComponent 的實作的程式碼時,會實作 ProductsContentProviderEntryPoint。最後,我們要利用 EntryPointAccessors 來從 Hilt components 中取得 instances。
class ProductsContentProvider : ContentProvider {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ProductsContentProviderEntryPoint {
fun productRepository(): ProductRepository
}
override fun query(...): Cursor {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val hiltEntryPoint =
EntryPointAccessors.fromApplication(appContext, ProductsContentProviderEntryPoint::class.java)
val productRepository = hiltEntryPoint.productRepository()
...
}
}我們可以發現其實 @AndroidEntryPoint 和 @Inject 就是幫我們處理以上這些步驟。但是,與 @EntryPoint 不同的是,@AndroidEntryPoint 會對每一個 Android class 產生個別的 Hilt component。
Hilt Components 和 @InstallIn
對於每一個加上 @AndroidEntryPoint 的 Android class,Hilt 都會產生個別的 Hilt component。然後,我們可以用 @InstallIn 來指定這些 Hilt components。
Hilt 對有支援的 Android classes 提供以下的 components:
| Hilt component | Injector for |
|---|---|
| SingletonComponent | Application, and also used by BroadcastReceiver |
| ActivityRetainedComponent | N/A |
| ViewModelComponent | ViewModel |
| ActivityComponent | Activity |
| FragmentComponent | Fragment |
| ViewComponent | View |
| ViewWithFragmentComponent | View annotated with @withFragmentBindings |
| ServiceComponent | Service |
Component Lifecycles
Hilt 對於每一個加上 @AndroidEntryPoint 的 Android class,都會產生個別的 Hilt component。下表列出這些 Hilt components 在對應的 Android class 中,何時被建立以及何時被銷毀。
| Generated component | Created at | Destroyed at |
|---|---|---|
| SingletonComponent | Application.onCreate() | Application destroyed |
| ActivityRetainedComponent | First Activity.onCreate() | Last Activity.onDestroy() |
| ViewModelComponent | ViewModel created | ViewModel destroyed |
| ActivityComponent | Activity.onCreate() | Activity.onDestroy() |
| FragmentComponent | Fragment.onAttach() | Fragment.onDestroy() |
| ViewComponent | View.super() | View destroyed |
| ViewWithFragmentComponent | View.super() | View destroyed |
| ServiceComponent | Service.onCreate() | Service.onDestroy() |
Component Scopes
所有的 bindings 預設都是 unscoped。這是說,每次從 Hilt component 中取得一個 instance 時,Hilt 都會建立新個 instance。
Hilt 也允許一個 binding 的 scope 對應到某個 component。也就是說,在這個 component 被建立到被銷毀之間,每次從這個 component 中取得的某的 type 的 instance 都會是同一個。
例如,我們在 ProductListAdapter 上加上 @ActivityScoped 的話,在 ProductListActivity 中的任何一個 Fragment 中要求 Hilt 執行 field injection 時,都會取得同一個 instance。
@ActivityScoped
class ProductListAdapter @Inject constructor() :
RecyclerView.Adapter<ProductListAdapter.ViewHolder>() {
}下表列出所有的 scopes。
| Android class | Generated component | Scope |
|---|---|---|
| Application | SingletonComponent | @Singleton |
| Activity | ActivityRetainedComponent | @ActivityRetainedScope |
| ViewModel | ViewModelComponent | @ViewModelScoped |
| Activity | ActivityComponent | @ActivityScoped |
| Fragment | FragmentComponent | @FragmentScoped |
| View | ViewComponent | @ViewScoped |
| View annotated with @withFragmentBindings | ViewWithFragmentComponent | @ViewScoped |
| Service | ServiceComponent | @ServiceScope |
Component Hierarchy
Hilt 會產生出很多 Hilt components。這些 components 之間是有階層關係的。當用 @InstallIn 指定一個 Hilt module 到某個 Hilt component 時,這個 component 的任何一個 child component 都可以使用這個 Hilt module 建立 instances。

結語
與 Dagger 相比,Hilt 使用起來確實方便許多。Hilt 自動幫我們產生一些有用的 components。如果使用 Dagger 的話,則要自己手動產生這些 components。也因此,使用 Hilt 時,我們可以省去這些程式碼。
參考
- Dependency injection with Hilt, Android developers.









