在 Kotlin 中使用 Dagger 会遇到的陷阱和优化方法

在 Kotlin 中使用 Dagger 会遇到的陷阱和优化方法

Dagger 在 Android 开发中相当流行,它是一个提供完全静态和在编译时生成代码的依赖注入框架,它解决了很多基于反射而实现的方案中所遇到的开发和性能问题。

为了让您更好地了解 Dagger 的工作原理,我们于 2019 年发布了一个 新的教程。本文将重点介绍如何 在 Kotlin 中使用 Dagger ,包括优化构建时间的 最佳实践 以及一些可能会遇到的问题。

Dagger 是通过 Java 的注解模型实现的,而 Kotlin 中注解的编写方式同 Java 的并不是一一对应的,这篇文章会重点介绍它们之间的不同之处,并且会介绍怎样轻松地将 Dagger 同 Kotlin 结合起来使用。

本文的写作灵感来自 Dagger issue 中的一些建议,这些建议直接代表了在 Kotlin 中使用 Dagger 的最佳实践和一些痛点。在此要感谢所有的 issue 贡献者。

提高构建效率

为了缩短构建时间,Dagger 在 v2.18 版本中新增了 对 gradle 增量注解处理  (gradle’s incremental annotation processing) 的支持。在 Dagger v2.24 版本中这个功能是默认启用的。如果您使用的是较低版本,您需要添加以下几行代码来激活该功能。

另外,您可以配置 Dagger 让它不要格式化生成的代码。这一选项是在 Dagger v2.18 版本中添加的,并且是 v2.23 版本中的默认行为 (不再生成格式化代码)。如果您使用的是较低的版本,同样可以添加下面的代码来禁用格式化代码以缩短构建时间。

在 build.gradle 中添加以下编译参数来提高 Dagger 在构建时的性能:

allprojects {
    afterEvaluate {
        extensions.findByName('kapt')?.arguments {
            arg("dagger.formatGeneratedSource", "disabled")
            arg("dagger.gradle.incremental", "enabled")
        }

    }
}

另外,如果您使用的是 Kotlin DSL 脚本文件,那么您需要在 build.gradle.kts 文件中包含以下内容:

kapt {
    arguments {
        arg("dagger.formatGeneratedSource", "disabled")
        arg("dagger.gradle.incremental", "enabled")
    }
}

使用 Qualifier 作为 field 的属性

在 Kotlin 的某个 property 上添加注解时,不清楚最终 Java 是否能够在该 property 的 field 或者 method 中获取到该注解。在注解之前添加 field: 前缀能够确保 qualifier 会作用到正确的地方 (查看 官方文档 获取更多详情)。

✅ 将 qualifier 作用于一个已注入的 field 的正确方法如下:

@Inject @field:MinimumBalance lateinit var minimumBalance: BigDecimal

❌ 下面的做法是不对的:

@Inject @MinimumBalance lateinit var minimumBalance: BigDecimal 
// @MinimumBalance 被忽略了

忘记添加 field: 如果在 Dagger 中存在一个不匹配该类型的实例,则可能会导致注入到错误的对象中。

在 Dagger v2.25 版本中已经修复该问题了,如果您使用的是该版本,之前这样的写法会出现问题,现在不会了。

@Inject @MinimumBalance lateinit var minimumBalance: BigDecimal 
// 已修复:@MinimumBalance 不再被忽略

使用静态的 @Provides 方法来提高性能

如果使用的是静态的 @Provides 方法,那么 Dagger 生成的代码将具有更好的性能。要达成这一效果,使用 Kotlin 中的 object 而不是 class,并在方法前添加 @JvmStatic 注解。这是您应该尽可能遵循的 最佳实践

@Module
object NetworkModule {

    @JvmStatic
    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}

如果需要使用抽象方法,则需要将 @JvmStatic 添加到 companion object 中,并增加 @Module 注解。

@Module
abstract class NetworkModule {

    @Binds abstract fun provideService(retrofitService: RetrofitService): Service

    @Module
    companion object {
    
        @JvmStatic
        @Provides
        fun provideOkHttpClient(): OkHttpClient {
            return return OkHttpClient.Builder().build()
        }
    }
}

或者,您可以将 object 模块代码抽取出来,并将其包含在抽象模块中:

@Module(includes = [OkHttpClientModule::java])
abstract class NetworkModule {

    @Binds abstract fun provideService(retrofitService: RetrofitService): Service

}

@Module
object OkHttpClientModule {

    @JvmStatic
    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
}

在 Dagger v2.25 版本中,您不再需要使用 @JvmStatic 来标记 @Provides 函数了,Dagger 会正确地识别它。

泛型注入

Kotlin 使用通配符编译泛型使 Kotlin API 和 Java 能一起使用。当某个参数或者字段的类型为 泛型 时,会在 Java 代码中自动生成。比如,Kotlin 的代码 List 参数就会在 Java 中显示为 List<? super Foo>。

但这种特性会导致在 Dagger 中出现问题,因为它期望类型是完全 (也称 invariant) 匹配的。使用 @JvmSuppressWildcards 将确保 Dagger 会看到没有通配符的类型。

当您使用 Dagger 的多重绑定特性时,这是一个常常会遇到的问题,比如:

class MyVMFactory @Inject constructor(
  private val vmMap: Map<String, @JvmSuppressWildcards 
Provider<ViewModel>>

) { 
    ... 
}

在 Dagger v2.25 版本中,您将不再需要使用 @JvmSuppressWildcards 了,Dagger 会正确地识别它。

内联方法体

Dagger 通过检查返回值类型来确定由 @Provides 方法配置的类型。在 Kotlin 函数中的返回类型是可选的,甚至 IDE 有时也会建议您重构代码使用内联方法体来隐藏返回值类型的声明。

如果推断的类型与您所期望的类型不一致,就会引起 bug 出现。我们来看一些例子:

如果要在 Dagger 中添加特定的类型,使用内联将是最好的选择。我们来看看在 Kotlin 中实现同样效果的另外一种方法:

@Provides 
fun provideNetworkPrinter() = NetworkPrinter()

@Provides 
fun provideNetworkPrinter(): NetworkPrinter = NetworkPrinter()

@Provides 
fun provideNetworkPrinter(): NetworkPrinter {
  return NetworkPrinter()
}

如果您需要提供接口的实现,则必须显示指定返回类型。不这样做的话会出问题:

@Provides
// 配置 Printer
fun providePrinter(): Printer = NetworkPrinter()

@Provides
// 配置 NetworkPrinter,不是一个普通的 Printer
fun providePrinter() = NetworkPrinter()

Dagger 基本上是同 Kotlin 兼容的,但是您仍然要注意,来确保代码不会出问题: 使用 @field: 来限定字段属性,内联方法体,当对集合进行注入时使用 @JvmSuppressWildcards 注解。

本次 Dagger 带来的优化不会带来额外的损耗,遵循这些最佳实践,诸如启用增量注释处理、禁用格式化设置以及使用静态 @Provides 方法等,可以缩短项目的构建时间。

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io Funder | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.
🍗