【Filt】KSP 初探之自动生成 Hilt MultiBindings
Filt 是 Fill+Hilt 的意思,用于简化在使用 Hilt
时注入接口所有的实现类的操作。
我先介绍下需求背景,首先需求上会对某一类事物与行为做一些统一的抽象,这些抽象被放在了一个单独的模块,app 层直接依赖这个模块并使用其中的实体与 UseCase
,这个抽象层会包含多个实现,每个实现也都是单独的模块,这些模块必然需要依赖抽象层模块(类似于插件化架构),并实现其中的实体与 UseCase
,app 层通过 runtimeOnly
的方式依赖了这些具体的实现模块,且抽象层不会依赖这些实现层。
那么抽象层就需要通过一些手段获取运行时接口的所有实现,一般来说我们通过 ServiceLoader
获取即可,但是 ServiceLoader
无法支持依赖注入,Hilt 虽然提供了获取接口所有实现的注入方式但使用起来非常麻烦,鉴于整体架构设计会导致出现很多类似的工作,所以考虑通过 KSP 自动生成 Hilt 所需要的相关代码,简化该场景的使用。
使用 Hilt @IntoSet 实现 MultiBindings
我们先看一下使用 Hilt 要怎么做。
首先定义一个接口。
interface DocParser {
fun parse(file: File): String
}
然后我们在 html 模块提供一个实现类:
class HtmlParser @Inject constructor() : DocParser {
override fun parse(file: File): String {
return "html parsed data"
}
}
@InstallIn(ActivityComponent::class)
@Module
abstract class HtmlParserModule {
@Binds
@IntoSet
abstract fun bind(input: HtmlParser): DocParser
}
再在 pdf 模块提供一个实现类:
class PdfParser @Inject constructor() : DocParser {
override fun parse(file: File): String {
return "pdf parsed data"
}
}
@InstallIn(ActivityComponent::class)
@Module
abstract class PdfParserModule {
@Binds
@IntoSet
abstract fun bind(input: PdfParser): DocParser
}
使用的时候就可以注入 Set 集合了:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var parsers: Set<@JvmSuppressWildcards DocParser>
}
对于 Kotlin 来说,还需要添加 @JvmSuppressWildcards
注解。
现在改用 Filt
@Filt(installIn = ActivityComponent::class) // 手动设置 installIn 参数
class HtmlParser @Inject constructor() : DocParser {
override fun parse(file: File): String {
return "html parsed data"
}
}
@Filt // 不设置则默认 SingletonComponent
class PdfParser @Inject constructor() : DocParser {
override fun parse(file: File): String {
return "pdf parsed data"
}
}
然后 build 一下,打开 build/generated/ksp/release/kotlin 目录就能看到自动生成的文件:
// HtmlParserBindModule.kt
@InstallIn(dagger.hilt.android.components.ActivityComponent::class)
@Module
public abstract class HtmlParserBindModule {
@Binds
@IntoSet
public abstract fun bind(input: HtmlParser): DocParser
}
// PdfParserBindModule.kt
@InstallIn(dagger.hilt.components.SingletonComponent::class)
@Module
public abstract class PdfParserBindModule {
@Binds
@IntoSet
public abstract fun bind(input: PdfParser): DocParser
}
生成的代码复合预期,与手动编写的一致。使用方式不变,仍然按照之前的方式使用。
@Filt
Filt 注解是对外暴漏的唯一注解,该注解包含两个参数。
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Filt(
val type: KClass<*> = Unit::class,
val installIn: KClass<*> = Unit::class,
)
type
表示需要注入的接口,因为一个类可以存在多个 SuperType
,如果包含多个 SuperType
时 Filt 处理器无法判断需要注入的是哪个接口,所以需要手动通过 type 参数指定。
installIn
参数同 dagger.hilt.InstallIn
注解,会将参数透传到生成的 BindModule 中,默认为 SingletonComponent
。
使用
首先需要配置项目的 KSP,具体步骤这里就不介绍了,按照官方给出的步骤即可。
https://kotlinlang.org/docs/ksp-quickstart.html
然后添加 Filt
的 KSP 和注解依赖就行了。
implementation("com.github.0xZhangKe.Filt:annotaions:1.0.3")
ksp("com.github.0xZhangKe.Filt:compiler:1.0.3")
另外可能还需要添加 jitpack 仓库。
repositories {
maven { setUrl("https://jitpack.io") }
}
相关代码都放在 GitHub 了:https://github.com/0xZhangKe/Filt