Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行 手动依赖项注入 要求您手动构造每个类及其依赖项,并借助容器重复使用和管理依赖项。
Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 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 31buildToolsVersion "30.0.3"defaultConfig {applicationId "com.example.android.hilt"minSdkVersion 16targetSdkVersion 31versionCode 1versionName "1.0"javaCompileOptions {annotationProcessorOptions {arguments["room.incremental"] = "true"}}}compileOptions {sourceCompatibility 1.8targetCompatibility 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'// Roomimplementation "androidx.room:room-runtime:2.3.0"kapt "androidx.room:room-compiler:2.3.0"// Hilt dependenciesimplementation "com.google.dagger:hilt-android:$hilt_version"kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
在项目中使用hilt。
Step1:使用@HiltAndroidApp
注解
新建继承自Application
的类并添加注解@HiltAndroidApp
,触发 Hilt 的代码生成,其中包括可以使用依赖项注入的应用基类。应用容器是应用的父容器,这意味着其他容器可以访问其提供的依赖项。
@HiltAndroidApp
class LogApplication : Application()
Step2:使用@AndroidEntryPoint
将依赖注入Android类。
在 Application 类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint
注解的其他 Android 类提供依赖项。Hilt 目前支持以下 Android 类:
- Application(通过使用 @HiltAndroidApp)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
如果您使用 @AndroidEntryPoint
为某个 Android 类添加注解,则还必须为依赖于该类的 Android 类添加注解。例如,如果您为某个 Fragment
添加注解,则还必须为使用该 Fragment 的所有 Activity 添加注解。
@AndroidEntryPoint
class LogsFragment : Fragment() { .... }
Step3:使用hilt进行字段注入
@Inject
注解让 Hilt 注入不同类型的实例。其实就是声明变量的时候用上这个注解
@AndroidEntryPoint
class LogsFragment : Fragment() {@Inject lateinit var logger: LoggerLocalDataSource@Inject lateinit var dateFormatter: DateFormatter...
}
Step4:Hilt提供实例。
step4-condition1:在构造器上利用@Inject
获取实例。
对于用@Inject
注解的变量,提供其实例时,如果是通过构造器创建的实例那么我们可以直接在构造器上利用@Inject
注解就可以让hilt为我们创建类的实例,比如下面的DateFormatter
/*** 通过构造器创建依赖*/
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的区别在于它的构造参数是有参数的。那么对于这种情况,我们还需要告诉hilt如何获取LogDao的实例。也就是说如果LogDao能通过构造器构建的话,直接添加@Inject
注解就可以了。但是这里的logDao是一个接口,而且它无法手动添加实现类(这个是Android room中的DAO)。所以我们需要使用其他的方式获取
@Singleton
class LoggerLocalDataSource @Inject constructor(private val logDao: LogDao) {...
}
step4-condition2:用 @Provides
提供实例
我们可以在 Hilt 模块中用 @Provides
注释函数,以告诉 Hilt 如何提供无法注入构造函数的 类型。hilt模块也就是用@Module
和 @InstallIn
注释的类的使用。无法通过对构造器添加@Inject
注解方式提供实例时通过@Module
和@InstallIn
(指定作用域)来声明提供对象实例的方式。 这个Module
是模块,我们需要使用模块向 Hilt 添加绑定,换句话说,就是告诉 Hilt 如何提供不同类型的实例。 在 Hilt 模块中,您需针对无法注入构造函数的类型(如项目中未包含的接口或类)添加绑定。例如 OkHttpClient - 您需要使用其构建器来创建实例。因为这里实际上是提供数据库操作,所以作用域应该是全局的,所以采用的是SingletonComponent
。这里还有其他的component
@InstallIn(SingletonComponent::class)
@Module
object DatabaseModule {
//这个可以是个class,但是在 Kotlin 中,只包含 @Provides 函数的模块可以是 object 类。
//这样,提供程序即会得到优化,并几乎可以内联在生成的代码中。/*** 用 @Provides 提供实例。我们可以在 Hilt 模块中用 @Provides 注释函数,* 以告诉 Hilt 如何提供无法注入构造函数的 类型。*/@Providesfun provideLogDao(database: AppDatabase): LogDao {
//return database.logDao()//Hilt 可从上述代码中得知,在提供 LogDao 的实例时需要执行 database.logDao()。//由于我们拥有 AppDatabase 作为传递依赖项,因此我们还需要告诉 Hilt 如何提供这种类型的实例。}//因为我们一直希望 Hilt 提供相同的数据库实例,所以我们用 @Singleton 注释 @Provides provideDatabase 方法。@Provides@Singletonfun provideDatabase(@ApplicationContext context: Context):AppDatabase{return Room.databaseBuilder(context,AppDatabase::class.java,"logging.db").build()}}
step4-condition3:用 @Binds
提供接口。
对于接口我们不能使用构造函数注入。 要告诉 Hilt 对接口使用什么实现,可以在 Hilt 模块内的函数上使用 @Binds
注释。@Binds
必须对抽象函数作出注释(因为该函数是抽象的,因此其中不包含任何代码,并且该类也必须是抽象的)。抽象函数的返回类型是我们要为其提供实现的接口(即 AppNavigator)。通过添加具有接口实现类型(即 AppNavigatorImpl)的唯一参数来指定实现。比如在MainActivity中我们依赖的接口
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {@Injectlateinit var navigator: AppNavigator....
}
所以对此我们需要新建module
使用@Binds
获取,如果类型有作用域,则@Binds 方法必须有作用域注释
//我们的新导航信息(即 AppNavigator)需要特定于 Activity 的信息
//(因为 AppNavigatorImpl 拥有 Activity 作为依赖项)。
// 因此,我们必须将其安装在 Activity 容器中,而不是安装在 Application 容器中,因为这是有关 Activity 的信息所在。
@InstallIn(ActivityComponent::class)
@Module
abstract class NavigationModule {@Bindsabstract fun provideNavigator(impl: AppNavigatorImpl):AppNavigator//参数为具体的实现类,所以要告知hilt如何提供实现类的实例。下面的实现类通过构造函数提供实例
}//======AppNavigatorImpl.ktx========////AppNavigatorImpl 会依赖于 FragmentActivity。由于系统会在 Activity 容器中提供 AppNavigator 实例
// (亦可用于 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 如何提供相同类型的不同实现(多个绑定),可以使用限定符。它的定义其实就是注解。
@Qualifier
annotation class InMemoryLogger
@Qualifier
annotation class DatabaseLogger
要比如对log的增删查提供一套基于内存的实现方式,那么定义接口
interface LogDataSource {fun addLog(msg: String)fun getAllLogs(callback: (List<Log>) -> Unit)fun removeLogs()
}
基于Room的实现如下,其实就是开篇提到的实现,只不过实现了该接口
@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()}}
}
基于内存的实现如下
@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()}
}
基于上面介绍,使用接口时我们定义实现类如下
@Module
@InstallIn(SingletonComponent::class)
abstract class LoggingDatabaseModule {@DatabaseLogger@Binds@Singletonabstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LogDataSource
}@Module
@InstallIn(ActivityComponent::class)
abstract class LoggingInMemoryModule {@InMemoryLogger@Binds@ActivityScopedabstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LogDataSource
}
可以看到我们定义了两个module
,之所以不是一个module是因为两种实现的作用域不一样。而且在InMemory的@Binds
方法上我们还加入了@ActivityScoped
,这个是必须加入的,因为实现类中指定了作用域。同理在这儿我们还加入了自定义的注解InMemoryLogger
,就是告诉hilt选择那种方式提供实例。如果不加限定符的话会报错。真正使用该接口时如下
class ButtonsFragment : Fragment() {@InMemoryLogger@Inject lateinit var logger: LogDataSource...
}
可以看到与Step3中的区别在于此处变量的类型为接口而不是具体的实现,其次加入了限定符。综上就是Hilt的基本使用
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap