在 Kotlin 中使用 Dagger 會遇到的陷阱和優化方法

Dagger 在 Android 開發中至關流行,它是一個提供徹底靜態和在編譯時生成代碼的依賴注入框架,它解決了不少基於反射而實現的方案中所遇到的開發和性能問題。html

爲了讓您更好地瞭解 Dagger 的工做原理,咱們於 2019 年發佈了一個 新的教程。本文將重點介紹如何 在 Kotlin 中使用 Dagger ,包括優化構建時間的 最佳實踐 以及一些可能會遇到的問題。java

Dagger 是經過 Java 的註解模型實現的,而 Kotlin 中註解的編寫方式同 Java 的並非一一對應的,這篇文章會重點介紹它們之間的不一樣之處,而且會介紹怎樣輕鬆地將 Dagger 同 Kotlin 結合起來使用。git

本文的寫做靈感來自 Dagger issue 中的一些建議,這些建議直接表明了在 Kotlin 中使用 Dagger 的最佳實踐和一些痛點。在此要感謝全部的 issue 貢獻者。github

提升構建效率

爲了縮短構建時間,Dagger 在 v2.18 版本中新增了 對 gradle 增量註解處理  (gradle’s incremental annotation processing) 的支持。在 Dagger v2.24 版本中這個功能是默認啓用的。若是您使用的是較低版本,您須要添加如下幾行代碼來激活該功能。api

另外,您能夠配置 Dagger 讓它不要格式化生成的代碼。這一選項是在 Dagger v2.18 版本中添加的,而且是 v2.23 版本中的默認行爲 (再也不生成格式化代碼)。若是您使用的是較低的版本,一樣能夠添加下面的代碼來禁用格式化代碼以縮短構建時間。框架

在 build.gradle 中添加如下編譯參數來提升 Dagger 在構建時的性能:jvm

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

    }
}

另外,若是您使用的是 Kotlin DSL 腳本文件,那麼您須要在 build.gradle.kts 文件中包含如下內容:ide

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<Foo> 參數就會在 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 方法等,能夠縮短項目的構建時間。

相關文章
相關標籤/搜索