张可的博客

【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