Android中Hilt的使用詳解
Hilt 是 Android 的依賴項(xiàng)注入庫,可減少在項(xiàng)目中執(zhí)行手動依賴項(xiàng)注入的樣板代碼。執(zhí)行手動依賴項(xiàng)注入要求您手動構(gòu)造每個類及其依賴項(xiàng),并借助容器重復(fù)使用和管理依賴項(xiàng)。
Hilt 通過為項(xiàng)目中的每個 Android 類提供容器并自動管理其生命周期,提供了一種在應(yīng)用中使用 DI(依賴項(xiàng)注入)的標(biāo)準(zhǔn)方法。Hilt 在熱門 DI 庫 Dagger 的基礎(chǔ)上構(gòu)建而成,因而能夠受益于 Dagger 的編譯時正確性、運(yùn)行時性能、可伸縮性和 Android Studio 支持。本篇只探討其使用方式,其步驟如下
在項(xiàng)目中引入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" }
在項(xiàng)目中使用hilt
Step1:使用@HiltAndroidApp注解
新建繼承自Application的類并添加注解@HiltAndroidApp,觸發(fā) Hilt 的代碼生成,其中包括可以使用依賴項(xiàng)注入的應(yīng)用基類。應(yīng)用容器是應(yīng)用的父容器,這意味著其他容器可以訪問其提供的依賴項(xiàng)。
@HiltAndroidApp class LogApplication : Application()
Step2:使用@AndroidEntryPoint將依賴注入Android類
在 Application 類中設(shè)置了 Hilt 且有了應(yīng)用級組件后,Hilt 可以為帶有 @AndroidEntryPoint 注解的其他 Android 類提供依賴項(xiàng)。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ù)的類型(如項(xiàng)目中未包含的接口或類)添加綁定。例如 OkHttpClient - 您需要使用其構(gòu)建器來創(chuàng)建實(shí)例。因?yàn)檫@里實(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 作為傳遞依賴項(xiàng),因此我們還需要告訴 Hilt 如何提供這種類型的實(shí)例。 } //因?yàn)槲覀円恢毕M?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ù)作出注釋(因?yàn)樵摵瘮?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 的信息 //(因?yàn)?AppNavigatorImpl 擁有 Activity 作為依賴項(xiàng))。 // 因此,我們必須將其安裝在 Activity 容器中,而不是安裝在 Application 容器中,因?yàn)檫@是有關(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 容器,因?yàn)?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是因?yàn)閮煞N實(shí)現(xiàn)的作用域不一樣。而且在InMemory的@Binds方法上我們還加入了@ActivityScoped,這個是必須加入的,因?yàn)閷?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-04Android中傳值Intent與Bundle的區(qū)別小結(jié)
這篇文章主要給大家總結(jié)介紹了關(guān)于Android中傳值Intent與Bundle的區(qū)別,文中通過示例代碼以及圖文介紹的非常詳細(xì),對各位Android開發(fā)者們具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03Android基礎(chǔ)之使用Fragment適應(yīng)不同屏幕和分辨率(分享)
以下是對Fragment的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友可以過來參考下2013-07-07