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.