前言
本文隶归于我概括收拾的Android常识体系的第四部分,归于DI中Hilt的进阶内容
假如您需求学习Hilt的基础内容,能够经过Android开发者官方供给的 MAD Skills 12-15篇 和 官方运用教程
文章依照以下内容打开:
文中涉及的代码和事例,均能够于 workshop 中取得。
非常重要:经过重复考虑,我删除了原先编写的关于Hilt工作原理和生成代码的部分。或许加上这部分,会让部分读者取得更深刻地了解,但我忧虑会让更多的读者陷入困境 而不敢运用。
正好像Google要创立Hilt一样,他们期望开发者以更简略的办法接入Dagger2,本篇文章也期望读者朋友能够先掌握怎么运用,并结合场景选用最佳实践计划。在此基础上再行了解背后的规划原理。
跨越 IOC容器的壁垒
运用依靠注入(DI)时,咱们需求它对 实例
、依靠联系
、 生命周期
进行办理,因此DI结构会构建一个容器,用于完成这些功用。这个容器咱们惯称为IOC容器。
在容器中,会依照咱们拟定的规矩:
- 创立实例
- 拜访实例
- 注入依靠
- 办理生命周期
但容器外也有拜访容器内部的需求,明显这里存在一道虚拟的 鸿沟、壁垒
。这种需求分为两类:
- 依靠注入客观需求的进口
- 系统中存在合理呈现的、非DI结构办理的实例,但它不期望损坏其他实例目标的
生命周期
、效果域唯一性
,即它的依靠期望交由DI结构办理
但请留意,IOC容器内部也存在着 鸿沟、壁垒
,这和它办理实例的机制有关,在Hilt(包括Dagger)中,最大颗粒度的内部壁垒是 Component
。
即使从外部打破IOC容器的壁垒,也只能进入某个特定的Component
运用EntryPoint跨越IOC容器壁垒
在Hilt中,咱们能够很便利地
- 运用接口界说 进入点(EntryPoint),并运用
@EntryPoint
注解使其收效; - 用
@InstallIn
注解指明拜访的Component; - 并利用
EntryPoints
完结拜访,打破容器壁垒
下面的代码展现了怎么界说:
UserComponent是自界说的Component,在下文中会具体打开
@EntryPoint
@InstallIn(UserComponent::class)
interface UserEntryPoint {
fun provideUserVO(): UserVO
}
下面的代码展现了怎么获取进入点,留意,您需求先取得对应的Component实例。
对于Hilt内建的Component,均有其获取办法,而自界说的Component,需从外界发起生命周期操控,同样会预留实例拜访路径
fun manualGet(): UserEntryPoint {
return EntryPoints.get(
UserComponentManager.instance.generatedComponent(),
UserEntryPoint::class.java
)
}
当获取进入点后,即可运用预界说的API,拜访容器内的目标实例。
自界说Scope、Component
部分业务场景中,Hilt内建的Scope和Component并不能完美支撑,此刻咱们需求进行自界说。
为了下文能够更顺畅的打开,咱们再花必定的笔墨对 Scope
、Component
、Module
的意义进行澄清。
Scope、Component、Module的实在意义
前文提到两点:
- DI结构需求
创立实例
、拜访实例
、注入依靠
、办理生命周期
- IOC容器内部也存在着
鸿沟、壁垒
,这和它办理实例的机制有关,在Hilt(包括Dagger)中,最大颗粒度的内部壁垒是Component
。
不难了解:
- 实例之间,也会存在依靠联系;
- DI结构需求办理内部实例的生命周期;
- 需求进行依靠注入的客户,本身也存在生命周期,它的依靠目标,应该结合实践需求被合理操控生命周期,防止生命周期走漏;
因此,呈现了 规模、效果域
即 Scope
的概念,它包括两个维度:实例的生命周期规模;实例之间的拜访界限。
并且DI结构经过Component操控内部目标的生命周期。
举一个比如描述,以Activity为例,Activity需求进行依靠注入,并且咱们不期望Activity自身需求的依靠呈现生命周期走漏,于是依照Activity的生命周期特色界说了:
-
ActivityRetainedScoped
ActivityRetainedComponent
,不受reCreate 影响 -
ActivityScoped
、ActivityComponent
,横竖屏切换等配置变化引起reCreate 开端新生命周期
并据此对 依靠目标实例
实施 生命周期
和 拜访规模
操控
能够记住以下三点定论:
- Activity实例依照
预定Scope对应的生命周期规模
创立、办理Component,拜访Component中的实例; - Component内的实例能够互相拜访,实例的生命周期和Component共同;
- Activity实例(需求依靠注入的客户)和 Component中的实例 能够拜访
父Component
中的实例,父Component的生命周期彻底包括子Component的生命周期
内建的Scope、Component联系参考:
而Module指导DI结构 创立实例
、选用实例进行注入
值得留意的是,Hilt(以及Dagger)能够经过 @Inject
注解类构造函数指导 创立实例
,此办法创立的实例的生命周期跟随宿主,与 经过Module办法
进行对比,存在生命周期办理粒度上的差异。
自界说
至此,已不难了解:因为有实践的生命周期规模办理需求,才会自界说。
为了便利行文以及编写演示代码,咱们举一个常见的比如:用户登录的生命周期。
一般的APP在规划中,用户登录后会持久化TOKEN,下次APP发动后验证TOKEN实在性和时效性,经过验证后用户仍坚持登录状态,直到TOKEN超时、登出。当APP退出时,能够等效以为用户登录生命周期结束。
明显,用户登录的生命周期彻底包括在APP生命周期(Singleton Scope)中,但略小于APP生命周期;和Activity生命周期无明显相关。
界说Scope
import javax.inject.Scope
@Scope
annotation class UserScope
就是这么简略。
界说Component
界说Component时,需求指明父Component和对应的Scope:
import dagger.hilt.DefineComponent
@DefineComponent(parent = SingletonComponent::class)
@UserScope
interface UserComponent {
}
Hilt需求以Builder构建Component,不只如此,一般构建Component时存在初始信息,例如:ActivityComponent需求供给Activity实例。
通常规划中,用户Component存在 用户基本信息、TOKEN
等初始信息
data class User(val name: String, val token: String) {
}
此刻,咱们能够在Builder中完结初始信息的注入:
import dagger.BindsInstance
import dagger.hilt.DefineComponent
@DefineComponent.Builder
interface Builder {
fun feedUser(@BindsInstance user: User?): Builder
fun build(): UserComponent
}
咱们以 @BindsInstance
注解标识需求注入的初始信息,留意合理操控其可空性,在后续的运用中,可空性需坚持共同
留意:办法名并不重要,选用习惯性命名即可,我习惯于将向容器喂入参数的API增加feed前缀
当咱们经过Hilt取得Builder实例时,即可操控Component的创立(即生命周期开端)
运用Manager办理Component
不难想象,Component的办理基本为模板代码,Hilt中供给了模板和接口类:
假如您想防止模板代码编写,能够界说扩展模块,运用APT、KCP、KSP生成
此处展现非线程安全的简略运用Demo
@Singleton
class UserComponentManager @Inject constructor(
private val builder: UserComponent.Builder
) : GeneratedComponentManager<UserComponent> {
companion object {
lateinit var instance: UserComponentManager
}
private var userComponent = builder
.feedUser(null)
.build()
fun onLogin(user: User) {
userComponent = builder.feedUser(user).build()
}
fun onLogout() {
userComponent = builder.feedUser(null).build()
}
override fun generatedComponent(): UserComponent {
return userComponent
}
}
您也能够界说如下的线程安全的Manager,并运用 ComponentSupplier
供给实例
class CustomComponentManager(
private val componentCreator: ComponentSupplier
) : GeneratedComponentManager<Any> {
@Volatile
private var component: Any? = null
private val componentLock = Any()
override fun generatedComponent(): Any {
if (component == null) {
synchronized(componentLock) {
if (component == null) {
component = componentCreator.get()
}
}
}
return component!!
}
}
您能够依据实践需求选择最适宜的办法进行办理,不再赘述。
在生命周期规模更小的Component中运用
至此,咱们现已完结了自界说Scope、Component的主要工作,经过Manager即可操控生命周期。
假如想在生命周期规模更小的Component中拜访 UserComponent中的目标实例,您需求谨记前文提到的三条定论。
该需求很合理,但下面的比如并不满意典型
此刻,您需求经过一个合理的Component完成拜访,例如在Activity中需求注入相关实例时。
因为 ActivityRetainedComponent
和 UserComponent
不存在父子联系,Scope没有交集,所以 需求找到共同的父Component进行协助,并经过EntryPoint打破壁垒 :
前文中,咱们将 UserComponentManager
划入 SingletonComponent
, 他是两种的共同父Component,此刻能够这样处理:
@Module
@InstallIn(ActivityRetainedComponent::class)
object AppModule {
@Provides
fun provideUserVO(manager: UserComponentManager):UserVO {
return UserEntryPoint.manualGet(manager.generatedComponent()).provideUserVO()
}
}
处理独立library的依靠初始化问题
此问题归于常见事例,经过研讨它的处理计划,咱们能够更深刻地了解前文内容,做到吃透。
当处理主工程时,没有代码阻隔,咱们能够很轻易的修正Application的代码,因此很多问题难以暴露。
例如,咱们能够在Application中经过注解标明依靠 (满意Singleton Scope前提) ,DI结构会协助咱们进行注入,在注入后能够编写逻辑代码,将目标赋值给全局变量,便能够 “便利” 的运用。
为便利下文表述,咱们称之 “计划1”
明显,这是有异味的代码,尽管它有用且便利。
因此,咱们选取一些场景来阐明该做法的坏处:
- 场景1:创立独立Library,其中运用Hilt作为DI结构,Library中存在自界说Component,需求初始化办理进口
- 场景2:项目选用了组件化,该Library依照途径包需求,途径包A集成、途径包B不集成
- 场景3:项目选用了Uni-App、React-Native等技能,该Library中存在实例由反射办法创立、不受Hilt办理,无法借助Hilt主动注入依靠
以上场景并不彼此孤立
在场景1中,咱们仍然能够经过 计划1
完结需求,但在场景2中便不再可行。
常规的组件化、插件化,都会完结代码阻隔&运用抽象,因此无法在主工程的Application中运用目标类。经过定制字节码东西曲线救国,则属实是大炮打蚊子、屎盆子镶金边
运用hilt的聚合才能处理问题
在 MAD Skills 系列文章的最终一篇中,简略提及了Hilt的聚合才能,它至少包括以下两个层面:
- 即使一个现已编译为aar的库,在被集成后,Hilt仍旧能够扫描该库中Hilt相关的内容,进行依靠图聚合
- Hilt生成的代码,仍旧存在着注解,这些注解能够被注解处理器、字节码东西识别、并进一步处理。能够是Hilt内建的处理器或您自界说的扩展处理器
依据第一个层面,咱们能够拟定一个约好:
子Library依照抽象接口供给Library初始化实例,主工程的Application经过DI结构获取后进行初始化
咱们将其称为计划2
例如,在Library中界说如下初始化类:
class LibInitializer @Inject constructor(
private val userComponentManager: UserComponentManager
) : Function1<Application, Any> {
override fun invoke(app: Application): Any {
UserComponentManager.instance = userComponentManager
return Unit
}
}
不难发现,他是计划1的变种,将依靠获取从Application中挪到了LibInitializer中
并约好绑定实例&集合注入, 仍旧在Library中编码 :
@InstallIn(SingletonComponent::class)
@Module
abstract class AppModuleBinds {
@Binds
@IntoSet
abstract fun provideLibInitializer(bind: LibInitializer): Function1<Application, Any>
}
在主工程的Application中:
@HiltAndroidApp
class App : Application() {
@Inject
lateinit var initializers: Set<@JvmSuppressWildcards Function1<Application, Any>>
override fun onCreate() {
super.onCreate()
initializers.forEach {
it(this)
}
}
}
如此即可满意场景1、场景2的需求。
但细心考虑一下,这种做法太 “强硬” 了,不只要求主工程的Application进行配合,并且需求当心的处理初始化代码的分配。
在场景3中,这些技能均有相适应的插件初始化进口;组件化插件化项目中,也具有相似的规划。随集成办法的不同,很或许造成 初始化逻辑遗漏或者重复 。
留意:重复初始化或许造成潜在的Scope走漏,滋生bug。
聚合才能+EntryPoint
前文中,咱们现已评论了运用EntryPoint打破IOC容器的壁垒,也体会了Hilt的聚合才能。而 SingletonComponent
作为内建Component,同样能够运用EntryPoint打破容器壁垒。
假如您对Hilt的源码或其规划有必定程度的了解,应当清楚:
内建Component均有对应的ComponentHolder,而SingletonComponent对应的Holder即为Application。
经过 Holder实例和 EntryPointAccessors
能够取得界说的 EntryPoint接口
为 SingletonComponent
自界说EntryPoint后,即可脱节Hilt自定注入的传递链而经过逻辑编码获取实例。
@EntryPoint
@InstallIn(SingletonComponent::class)
interface UserComponentEntryPoint {
companion object {
fun manualGet(context: Context): UserComponentEntryPoint {
return EntryPointAccessors.fromApplication(
context, UserComponentEntryPoint::class.java
)
}
}
fun provideBuilder(): UserComponent.Builder
fun provideManager():UserComponentManager
}
经过这一办法,咱们只需求取得Context即可打破壁垒拜访容器内部实例,Hilt不再束缚Library的初始化办法。
至此,您能够在原先的Library初始化模块中,按需自由的增加逻辑!
留意:Builder由Hilt生成完成,无法干涉其生命周期,故每次调用时生成新的实例,从一般的编码需求,获取Manager实例即可。您能够在WorkShop项目中取得验证
问题衍生
在场景3中,咱们继续进行衍生:
Library作为动态插件,并不直接集成,而是经过插件化技能,动态集成启用功用。又该怎么处理呢?
在MAD Skills系列文章的第四篇中,简略提及了Hilt的扩展才能。考虑到篇幅以及AAB(Dynamic Feature)、插件化的背景,咱们将在下一篇文章中对该问题打开处理计划的评论。
末
先做到正确运用,再逐步了解原理。我会在后续系列文章中,同读者淋漓尽致的评论Hilt的工作原理和完成原理。