前言

本文隶归于我概括收拾的Android常识体系的第四部分,归于DI中Hilt的进阶内容

Hilt进阶--一文吃透Hilt自定义与跨壁垒

假如您需求学习Hilt的基础内容,能够经过Android开发者官方供给的 MAD Skills 12-15篇 和 官方运用教程

文章依照以下内容打开:

Hilt进阶--一文吃透Hilt自定义与跨壁垒

文中涉及的代码和事例,均能够于 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并不能完美支撑,此刻咱们需求进行自界说。

为了下文能够更顺畅的打开,咱们再花必定的笔墨对 ScopeComponentModule 的意义进行澄清。

Scope、Component、Module的实在意义

前文提到两点:

  • DI结构需求 创立实例拜访实例注入依靠办理生命周期
  • IOC容器内部也存在着 鸿沟、壁垒,这和它办理实例的机制有关,在Hilt(包括Dagger)中,最大颗粒度的内部壁垒是 Component

不难了解:

  • 实例之间,也会存在依靠联系;
  • DI结构需求办理内部实例的生命周期;
  • 需求进行依靠注入的客户,本身也存在生命周期,它的依靠目标,应该结合实践需求被合理操控生命周期,防止生命周期走漏

因此,呈现了 规模、效果域Scope 的概念,它包括两个维度:实例的生命周期规模;实例之间的拜访界限。

并且DI结构经过Component操控内部目标的生命周期。

举一个比如描述,以Activity为例,Activity需求进行依靠注入,并且咱们不期望Activity自身需求的依靠呈现生命周期走漏,于是依照Activity的生命周期特色界说了:

  • ActivityRetainedScoped ActivityRetainedComponent,不受reCreate 影响
  • ActivityScopedActivityComponent,横竖屏切换等配置变化引起reCreate 开端新生命周期

并据此对 依靠目标实例 实施 生命周期拜访规模 操控

能够记住以下三点定论:

  • Activity实例依照 预定Scope对应的生命周期规模 创立、办理Component,拜访Component中的实例;
  • Component内的实例能够互相拜访,实例的生命周期和Component共同;
  • Activity实例(需求依靠注入的客户)和 Component中的实例 能够拜访 父Component中的实例,父Component的生命周期彻底包括子Component的生命周期

内建的Scope、Component联系参考:

Hilt进阶--一文吃透Hilt自定义与跨壁垒

而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中需求注入相关实例时。 因为 ActivityRetainedComponentUserComponent 不存在父子联系,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的工作原理和完成原理。