Anvil - 使集成Daggar 2更容易的KCP - 1

490 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

"When all you have is an anvil, every problem looks like a hammer."  - Abraham Maslow

Anvil是一个Kotlin编译器插件, 使得通过Dagger进行依赖注入更加容易, 它自动地合并了Dagger的module和component接口. 简而言之, 它没有手动的添加module到Dagger component上面, 也没有使得Dagger component扩展全部component接口, 这些module和接口能够自动地包含进component:

@Module
@ContributesTo(AppScope::class)
class DaggerModule { .. }

@ContributesTo(AppScope::class)
interface ComponentInterface {
  fun getSomething(): Something
  fun injectActivity(activity: MyActivity)
}

// The real Dagger component.
@MergeComponent(AppScope::class)
interface AppComponent

生成的AppComponent接口在Dagger眼中看起来是这样子:

@Component(modules = [DaggerModule::class])
interface AppComponent : ComponentInterface

注意: AppComponent自动地包含了DaggerModule且扩展了ComponentInterface.

设置

这个插件由Gradle插件和Kotlin编译器插件(Kotlin Compiler Plugin, 也即KCP)组成. Gradle插件自动地添加KCP和注解依赖. 它需要应用于全部的modules中, 要么贡献类到依赖图谱, 要么合并类:

plugins {
  id 'com.squareup.anvil' version "${latest_version}"
}

或者使用旧的方式应用插件:

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "com.squareup.anvil:gradle-plugin:${latest_version}"
  }
}

apply plugin: 'com.squareup.anvil'

快速开始

有3个重要注解和Anvil一起工作.

@ContributesTo用于添加到Dagger module和component接口, 这些接口应该被包含进Dagger component. 拥有这个注解的类, 只有它们位于编译类路径上, 就会自动地被KCP合并.

@MergeComponent用于取代Dagger注解@Component. Anvil将生成Dagger注解并且自动地包含全部贡献到相同作用域的modules和component接口.

@MergeSubcomponent相似于@MergeComponent, 用于取代subcomponent.

作用域

作用域类只是标记. 示例代码中的AppScope类看起来像这样子:

abstract class AppScope private constructor()

这些作用域类助力Anvil在Dagger component, Dagger modules和其它在导入的component接口之间建立联系.

作用域类独立于Dagger的作用域. 为Dagger component设置作用域依然是必要的, 例如.

@Singleton
@MergeComponent(AppScope::class)
interface AppComponent

贡献绑定

@ContributesBinding注解对被注解类生成了Dagger的绑定方法, 并且贡献了这个绑定方法进行给定的作用域. 想像下面的例子:

interface Authenticator

class RealAuthenticator @Inject constructor() : Authenticator

@Module
@ContributesTo(AppScope::class)
abstract class AuthenticatorModule {
  @Binds abstract fun bindRealAuthenticator(authenticator: RealAuthenticator): Authenticator
}

如果在注入Authenticator的时候总是要使用RealAuthenticator, 就会有很多模板代码. 可以通过@ContributesBinding注解取代整个Dagger module. 等价代码如下:

interface Authenticator

@ContributesBinding(AppScope::class)
class RealAuthenticator @Inject constructor() : Authenticator

@ContributesBinding也支持限定符. 可以用任何限定符注解这个类, 而且生成的绑定方法会保留限定符, 比如:

@ContributesBinding(AppScope::class)
@Named("Prod")
class RealAuthenticator @Inject constructor() : Authenticator

// Will generate:
@Binds @Named("Prod") 
abstract fun bindRealAuthenticator(authenticator: RealAuthenticator): Authenticator

贡献多重绑定

相似于贡献绑定, @ContributesMultibinding将会为被注解类生成多重绑定方法. 限定符也和常规绑定一样的方式被支持.

@ContributesMultibinding(AppScope::class)
@Named("Prod")
class MainListener @Inject constructor() : Listener

// Will generate this binding method.
@Binds
@IntoSet
@Named("Prod")
abstract fun bindMainListener(listener: MainListener): Listener

如果该类注解了@MapKey注解, 那么Anvil将生成映射多重绑定方法, 而非将元素添加进集合:

@MapKey
annotation class BindingKey(val value: String)

@ContributesMultibinding(AppScope::class)
@BindingKey("abc")
class MainListener @Inject constructor() : Listener

// Will generate this binding method.
@Binds
@IntoMap
@BindingKey("abc")
abstract fun bindMainListener(listener: MainListener): Listener

排除

Dagger modules和component接口可以在2个不同层面进行排除.

类总是可以被别的类取代. 这对于给插桩测试提供不同绑定module而言, 是极其有用的, 比如.

@Module
@ContributesTo(
    scope = AppScope::class,
    replaces = [DevelopmentApplicationModule::class]
)
object DevelopmentApplicationTestModule {
  @Provides
  fun provideEndpointSelector(): EndpointSelector = TestingEndpointSelector
}

KCP会找到类和类路径. 将DevelopmentApplicationModuleDevelopmentApplicationTestModule添加进Dagger图谱会导致重复绑定. Anvil看到测试module想要取代另外一个module, 进而会忽略它. 这种取代规则对于全部包含类和类路径的应用具有全局效应.

应用可以各自排除Dagger modules和component接口, 在没有影响其它应用的前提下.

@MergeComponent(
  scope = AppScope::class,
  exclude = [
    DaggerModule::class
  ]
)
interface AppComponent

在完美的构建图谱中, 这个特性是不可能需要的. 然而, 由于遗留的module, 错误的导入, 以及深度嵌套依赖链, 应用可能需要利用这个特性. 排除规则按照它所显示的那样工作. 在这个特定的例子当中DaggerModule希望贡献到这个作用域, 但它已经被排除了这个component, 由此它并不会被添加进来.

生成Dagger工厂

Anvil允许生成Factory类, 该类通常被Dagger注解处理器为@Provides方法, @Inject构建器和@Inject字段生成. 这个特性的好处是不必在这个module中启用Dagger注解处理器. 这经常意味着可以跳过KAPT和stub生成任务. 此外, Anvil生成Kotlin代码而非Java代码, 这允许Gradle跳过Java编译任务. 导致的结果就是更快的构建.

anvil {
  generateDaggerFactories = true // default is false
}

在我们的代码库中, 与使用Dagger注释处理器相比, 使用此新Anvil特性的Dagger模块构建速度快65%:

Stub generationKaptJavacKotlincSum
Dagger12.97640.3778.57110.24172.165
Anvil006.96517.74824.713

对于完整的应用构建, 我们测量平均节省了16%.

benchmark_dagger_factories.png

这个特性只能在没有编译任何Dagger component的Gradle modules中开启. 因为Anvil只处理Kotlin代码, 在混合了Kotlin/Java源码的module中不应该启用.

当你启用这个特性的时候, 不要忘记移除Dagger注解处理器. 但应该保留其它全部依赖.