Hilt 是 Android Jetpack 中的依赖注入框架。依赖注入是构建大型项目必不可少的技术手段,通过依赖注入我们解耦了对象的生产与消费,实现了关注点分离的设计目标,同时也方便单元测试。
Hilt 继承了 Dagger 编译期检查等优秀特性,通过更加易用的 API 降低了 Dagger 的使用门槛。它提供了 @AndroidEntryPoint 等注解为 Android 原生组件的 DI 提供了开箱即用的使用体验。自定义的 Activity,Fragment,View,Service 等在添加 @AndroidEntryPoint 后,其内部的 @Inject 成员会在适当的生命周期节点被自动注入实例。类似的还有 @HiltViewModel 也提供了对 ViewModel 的注入能力。
同时仍然有一些不支持 @AndroidEntryPoint 的组件,比如 ContentProvider 等,对于它们的 DI 我们虽然可以降级成 Dagger 的方式对其手动 inject ,但是这不利于 Hilt 中创建的 Component 的复用。那么今天介绍的 @EnterPoint 就可以派上用场了。
以 ContentProvider 的注入为例,
private const val LOGS_TABLE = "logs"
private const val AUTHORITY = "com.example.android.hilt.provider"
private const val CODE_LOGS_DIR = 1
private const val CODE_LOGS_ITEM = 2
class LogsContentProvider: ContentProvider() {
private val matcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, LOGS_TABLE, CODE_LOGS_DIR)
addURI(AUTHORITY, "$LOGS_TABLE/*", CODE_LOGS_ITEM)
}
override fun onCreate(): Boolean {
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val code: Int = matcher.match(uri)
return if (code == CODE_LOGS_DIR || code == CODE_LOGS_ITEM) {
val appContext = context?.applicationContext ?: throw IllegalStateException()
val logDao: LogDao = getLogDao(appContext)
val cursor: Cursor? = if (code == CODE_LOGS_DIR) {
logDao.selectAllLogsCursor()
} else {
logDao.selectLogById(ContentUris.parseId(uri))
}
cursor?.setNotificationUri(appContext.contentResolver, uri)
cursor
} else {
throw IllegalArgumentException("Unknown URI: $uri")
}
}
}
如上,LogsContentProvider 用来从数据库查新日志,我们使用 Room 定义 LogDao,用来对数据库进行 query
@Dao
interface LogDao {
...
@Query("SELECT * FROM logs ORDER BY id DESC")
fun selectAllLogsCursor(): Cursor
@Query("SELECT * FROM logs WHERE id = :id")
fun selectLogById(id: Long): Cursor?
}
在 LogsContentProvider 中定义了 getLogDao(appContext) 获取 Dao 的实例。接下里我们通过 @EnterPoint 通过 Hilt 注入这个 Dao 实例。
首先定义一个提供 Dao 的接口,并添加 @EnterPoint
class LogsContentProvider: ContentProvider() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LogsContentProviderEntryPoint {
fun logDao(): LogDao
}
...
}
安装官方的推荐做法,@EntryPoint 接口最好定义在需要使用它的类中,比如例子中定义在 LogsContentProvider 内,同时要添加 @InstallIn ,只用来指定我们在接口中提供的实例所处的 Scope ,比如例子中我们希望 LogDao 是 Application 级别的实例,所以需要 installIn 到 ApplicationComponent 。 我们使用 @AndroidEntryPoint 不需要添加 @InstallIn,是因为 Hilt 内部已经做了处理能够根据被注入对象确定 Scope ,比如 Activity 的注入实例默认 Scope 就是 ActivityComponent 。
定义 @EnterPoint 之后,我们可以通过 EnterPointAccessors 的静态方法获取对应实例,比如 LogContentProvider 中的 getLogDao(appContext: Context) 的实现:
class LogsContentProvider: ContentProvider() {
...
private fun getLogDao(appContext: Context): LogDao {
val hiltEntryPoint = EntryPointAccessors.fromApplication(
appContext,
LogsContentProviderEntryPoint::class.java
)
return hiltEntryPoint.logDao()
}
}
EnterPointAccessors 提供了四个静态方法,分别用来从不同 Scope 中获取示例,传入的第一个参数需要与我们在 @InstallIn 中指定的 Scope 相匹配,否则将无法正确获取实例。例子中我们将 LogDao 注册到 ApplicationScope ,所以我们使用 fromApplication 获取实例。
|