Android中Hilt的使用詳解
Hilt 是 Android 的依賴項注入庫,可減少在項目中執(zhí)行手動依賴項注入的樣板代碼。執(zhí)行手動依賴項注入要求您手動構(gòu)造每個類及其依賴項,并借助容器重復(fù)使用和管理依賴項。
Hilt 通過為項目中的每個 Android 類提供容器并自動管理其生命周期,提供了一種在應(yīng)用中使用 DI(依賴項注入)的標(biāo)準(zhǔn)方法。Hilt 在熱門 DI 庫 Dagger 的基礎(chǔ)上構(gòu)建而成,因而能夠受益于 Dagger 的編譯時正確性、運(yùn)行時性能、可伸縮性和 Android Studio 支持。本篇只探討其使用方式,其步驟如下
在項目中引入Hilt
在project/build.gradle下加入kotlin和hilt的插件
buildscript {
ext.kotlin_version = '1.5.31'
ext.hilt_version = '2.40'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
//kotlin編譯插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//hilt編譯插件
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}在app/build.gradle下加入kotlin和hilt
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.android.hilt"
minSdkVersion 16
targetSdkVersion 31
versionCode 1
versionName "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments["room.incremental"] = "true"
}
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
// Room
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
// Hilt dependencies
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}在項目中使用hilt
Step1:使用@HiltAndroidApp注解
新建繼承自Application的類并添加注解@HiltAndroidApp,觸發(fā) Hilt 的代碼生成,其中包括可以使用依賴項注入的應(yīng)用基類。應(yīng)用容器是應(yīng)用的父容器,這意味著其他容器可以訪問其提供的依賴項。
@HiltAndroidApp class LogApplication : Application()
Step2:使用@AndroidEntryPoint將依賴注入Android類
在 Application 類中設(shè)置了 Hilt 且有了應(yīng)用級組件后,Hilt 可以為帶有 @AndroidEntryPoint 注解的其他 Android 類提供依賴項。Hilt 目前支持以下 Android 類:
- Application(通過使用 @HiltAndroidApp)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
如果您使用 @AndroidEntryPoint 為某個 Android 類添加注解,則還必須為依賴于該類的 Android 類添加注解。例如,如果您為某個 Fragment 添加注解,則還必須為使用該 Fragment 的所有 Activity 添加注解。
@AndroidEntryPoint
class LogsFragment : Fragment() { .... }Step3:使用hilt進(jìn)行字段注入
@Inject 注解讓 Hilt 注入不同類型的實(shí)例。其實(shí)就是聲明變量的時候用上這個注解
@AndroidEntryPoint
class LogsFragment : Fragment() {
@Inject lateinit var logger: LoggerLocalDataSource
@Inject lateinit var dateFormatter: DateFormatter
...
}Step4:Hilt提供實(shí)例
step4-condition1:在構(gòu)造器上利用@Inject獲取實(shí)例。
對于用@Inject注解的變量,提供其實(shí)例時,如果是通過構(gòu)造器創(chuàng)建的實(shí)例那么我們可以直接在構(gòu)造器上利用@Inject注解就可以讓hilt為我們創(chuàng)建類的實(shí)例,比如下面的DateFormatter
/**
* 通過構(gòu)造器創(chuàng)建依賴
*/
class DateFormatter @Inject constructor() {
@SuppressLint("SimpleDateFormat")
private val formatter = SimpleDateFormat("d MMM yyyy HH:mm:ss")
fun formatDate(timestamp: Long): String {
return formatter.format(Date(timestamp))
}
}再比如Step3中的logger。它與DateFormatter的區(qū)別在于它的構(gòu)造參數(shù)是有參數(shù)的。那么對于這種情況,我們還需要告訴hilt如何獲取LogDao的實(shí)例。也就是說如果LogDao能通過構(gòu)造器構(gòu)建的話,直接添加@Inject注解就可以了。但是這里的logDao是一個接口,而且它無法手動添加實(shí)現(xiàn)類(這個是Android room中的DAO)。所以我們需要使用其他的方式獲取
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {
...
}step4-condition2:用 @Provides 提供實(shí)例
我們可以在 Hilt 模塊中用 @Provides 注釋函數(shù),以告訴 Hilt 如何提供無法注入構(gòu)造函數(shù)的 類型。hilt模塊也就是用@Module 和 @InstallIn 注釋的類的使用。無法通過對構(gòu)造器添加@Inject注解方式提供實(shí)例時通過@Module和@InstallIn(指定作用域)來聲明提供對象實(shí)例的方式。 這個Module是模塊,我們需要使用模塊向 Hilt 添加綁定,換句話說,就是告訴 Hilt 如何提供不同類型的實(shí)例。 在 Hilt 模塊中,您需針對無法注入構(gòu)造函數(shù)的類型(如項目中未包含的接口或類)添加綁定。例如 OkHttpClient - 您需要使用其構(gòu)建器來創(chuàng)建實(shí)例。因為這里實(shí)際上是提供數(shù)據(jù)庫操作,所以作用域應(yīng)該是全局的,所以采用的是SingletonComponent。這里還有其他的component

@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
//這個可以是個class,但是在 Kotlin 中,只包含 @Provides 函數(shù)的模塊可以是 object 類。
//這樣,提供程序即會得到優(yōu)化,并幾乎可以內(nèi)聯(lián)在生成的代碼中。
/**
* 用 @Provides 提供實(shí)例。我們可以在 Hilt 模塊中用 @Provides 注釋函數(shù),
* 以告訴 Hilt 如何提供無法注入構(gòu)造函數(shù)的 類型。
*/
@Provides
fun provideLogDao(database: AppDatabase): LogDao {
//
return database.logDao()
//Hilt 可從上述代碼中得知,在提供 LogDao 的實(shí)例時需要執(zhí)行 database.logDao()。
//由于我們擁有 AppDatabase 作為傳遞依賴項,因此我們還需要告訴 Hilt 如何提供這種類型的實(shí)例。
}
//因為我們一直希望 Hilt 提供相同的數(shù)據(jù)庫實(shí)例,所以我們用 @Singleton 注釋 @Provides provideDatabase 方法。
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context):AppDatabase{
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"logging.db"
).build()
}
}step4-condition3:用 @Binds 提供接口。
對于接口我們不能使用構(gòu)造函數(shù)注入。 要告訴 Hilt 對接口使用什么實(shí)現(xiàn),可以在 Hilt 模塊內(nèi)的函數(shù)上使用 @Binds 注釋。@Binds必須對抽象函數(shù)作出注釋(因為該函數(shù)是抽象的,因此其中不包含任何代碼,并且該類也必須是抽象的)。抽象函數(shù)的返回類型是我們要為其提供實(shí)現(xiàn)的接口(即 AppNavigator)。通過添加具有接口實(shí)現(xiàn)類型(即 AppNavigatorImpl)的唯一參數(shù)來指定實(shí)現(xiàn)。比如在MainActivity中我們依賴的接口
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var navigator: AppNavigator
....
}所以對此我們需要新建module使用@Binds獲取,如果類型有作用域,則@Binds 方法必須有作用域注釋
//我們的新導(dǎo)航信息(即 AppNavigator)需要特定于 Activity 的信息
//(因為 AppNavigatorImpl 擁有 Activity 作為依賴項)。
// 因此,我們必須將其安裝在 Activity 容器中,而不是安裝在 Application 容器中,因為這是有關(guān) Activity 的信息所在。
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {
@Binds
abstract fun provideNavigator(impl: AppNavigatorImpl):AppNavigator
//參數(shù)為具體的實(shí)現(xiàn)類,所以要告知hilt如何提供實(shí)現(xiàn)類的實(shí)例。下面的實(shí)現(xiàn)類通過構(gòu)造函數(shù)提供實(shí)例
}
//======AppNavigatorImpl.ktx========//
//AppNavigatorImpl 會依賴于 FragmentActivity。由于系統(tǒng)會在 Activity 容器中提供 AppNavigator 實(shí)例
// (亦可用于 Fragment 容器和 View 容器,因為 NavigationModule 會安裝在 ActivityComponent 中),所以 FragmentActivity 目前可用
class AppNavigatorImpl @Inject constructor(private val activity: FragmentActivity) : AppNavigator {
override fun navigateTo(screen: Screens) {
val fragment = when (screen) {
Screens.BUTTONS -> ButtonsFragment()
Screens.LOGS -> LogsFragment()
}
activity.supportFragmentManager.beginTransaction()
.replace(R.id.main_container, fragment)
.addToBackStack(fragment::class.java.canonicalName)
.commit()
}
}step4-condition4:使用限定符
要告訴 Hilt 如何提供相同類型的不同實(shí)現(xiàn)(多個綁定),可以使用限定符。它的定義其實(shí)就是注解。
@Qualifier annotation class InMemoryLogger @Qualifier annotation class DatabaseLogger
要比如對log的增刪查提供一套基于內(nèi)存的實(shí)現(xiàn)方式,那么定義接口
interface LogDataSource {
fun addLog(msg: String)
fun getAllLogs(callback: (List<Log>) -> Unit)
fun removeLogs()
}基于Room的實(shí)現(xiàn)如下,其實(shí)就是開篇提到的實(shí)現(xiàn),只不過實(shí)現(xiàn)了該接口
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao):LogDataSource {
private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
private val mainThreadHandler by lazy {
Handler(Looper.getMainLooper())
}
override fun addLog(msg: String) {
executorService.execute {
logDao.insertAll(
Log(
msg,
System.currentTimeMillis()
)
)
}
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
executorService.execute {
val logs = logDao.getAll()
mainThreadHandler.post { callback(logs) }
}
}
override fun removeLogs() {
executorService.execute {
logDao.nukeTable()
}
}
}基于內(nèi)存的實(shí)現(xiàn)如下
@ActivityScoped
class LoggerInMemoryDataSource @Inject constructor():LogDataSource {
private val logs = LinkedList<Log>()
override fun addLog(msg: String) {
logs.addFirst(Log(msg, System.currentTimeMillis()))
}
override fun getAllLogs(callback: (List<Log>) -> Unit) {
callback(logs)
}
override fun removeLogs() {
logs.clear()
}
}基于上面介紹,使用接口時我們定義實(shí)現(xiàn)類如下
@Module
@InstallIn(SingletonComponent::class)
abstract class LoggingDatabaseModule {
@DatabaseLogger
@Binds
@Singleton
abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LogDataSource
}
@Module
@InstallIn(ActivityComponent::class)
abstract class LoggingInMemoryModule {
@InMemoryLogger
@Binds
@ActivityScoped
abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LogDataSource
}可以看到我們定義了兩個module,之所以不是一個module是因為兩種實(shí)現(xiàn)的作用域不一樣。而且在InMemory的@Binds方法上我們還加入了@ActivityScoped,這個是必須加入的,因為實(shí)現(xiàn)類中指定了作用域。同理在這兒我們還加入了自定義的注解InMemoryLogger,就是告訴hilt選擇那種方式提供實(shí)例。如果不加限定符的話會報錯。真正使用該接口時如下
class ButtonsFragment : Fragment() {
@InMemoryLogger
@Inject lateinit var logger: LogDataSource
...
}可以看到與Step3中的區(qū)別在于此處變量的類型為接口而不是具體的實(shí)現(xiàn),其次加入了限定符。綜上就是Hilt的基本使用
到此這篇關(guān)于Android中Hilt的使用詳解的文章就介紹到這了,更多相關(guān)Android Hilt內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 判斷日期是否在一年以內(nèi)的算法實(shí)例
下面小編就為大家?guī)硪黄狝ndroid 判斷日期是否在一年以內(nèi)的算法實(shí)例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04
Android中傳值Intent與Bundle的區(qū)別小結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于Android中傳值Intent與Bundle的區(qū)別,文中通過示例代碼以及圖文介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
Android基礎(chǔ)之使用Fragment適應(yīng)不同屏幕和分辨率(分享)
以下是對Fragment的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過來參考下2013-07-07

