Android:Hilt 依賴注入

Photo by Gemma Evans on Unsplash
Photo by Gemma Evans on Unsplash
Hilt 是基於 Dagger 且設計在 Android 上使用的 dependency injection library。所以在開發 Android 時,使用 Hilt 會比使用 Dagger 更加地方便。本文章將介紹如何使用 Hilt。

Hilt 是基於 Dagger 且設計在 Android 上使用的 dependency injection library。所以在開發 Android 時,使用 Hilt 會比使用 Dagger 更加地方便。本文章將介紹如何使用 Hilt。

完整程式碼可以在 下載。

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 ProductRepositoryclass 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 如下:

@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 componentInjector for
SingletonComponentApplication, and also used by BroadcastReceiver
ActivityRetainedComponentN/A
ViewModelComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView annotated with @withFragmentBindings
ServiceComponentService
Hilt components from Android developers.

Component Lifecycles

Hilt 對於每一個加上 @AndroidEntryPoint 的 Android class,都會產生個別的 Hilt component。下表列出這些 Hilt components 在對應的 Android class 中,何時被建立以及何時被銷毀。

Generated componentCreated atDestroyed at
SingletonComponentApplication.onCreate()Application destroyed
ActivityRetainedComponentFirst Activity.onCreate()Last Activity.onDestroy()
ViewModelComponentViewModel createdViewModel destroyed
ActivityComponentActivity.onCreate()Activity.onDestroy()
FragmentComponentFragment.onAttach()Fragment.onDestroy()
ViewComponentView.super()View destroyed
ViewWithFragmentComponentView.super()View destroyed
ServiceComponentService.onCreate()Service.onDestroy()
The lifecycles of Hilt components from Android developers.

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 classGenerated componentScope
ApplicationSingletonComponent@Singleton
ActivityActivityRetainedComponent@ActivityRetainedScope
ViewModelViewModelComponent@ViewModelScoped
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View annotated with
@withFragmentBindings
ViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScope
The scopes of Hilt components from Android developers.

Component Hierarchy

Hilt 會產生出很多 Hilt components。這些 components 之間是有階層關係的。當用 @InstallIn 指定一個 Hilt module 到某個 Hilt component 時,這個 component 的任何一個 child component 都可以使用這個 Hilt module 建立 instances。

Hierarchy of the components that Hilt generates from <a href=
Hierarchy of the components that Hilt generates from Android developers.

結語

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

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Hans-Jurgen Mager on Unsplash
Read More

Kotlin Coroutine 教學

Kotlin 的 coroutine 是用來取代 thread。它不會阻塞 thread,而且還可以被取消。Coroutine core 會幫你管理 thread 的數量,讓你不需要自行管理,這也可以避免不小心建立過多的 thread。
Read More