學習一下 nowinandroid 的構建指令碼

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


⚠️本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

nowinandroid 專案是谷歌開源的示例專案,它遵循 Android 設計和開發的最佳實踐,並旨在成為開發人員的有用參考

這個專案在架構演進,模組化方案,單元測試,Jetpack Compose,啟動優化等多個方面都做了很好的示例,的確是一個值得學習的好專案

今天我們來學習一下 nowinandroid 專案的構建指令碼,看一下都有哪些值得學習的地方

gradle.properties 中的配置

要看一個專案的構建指令碼,我們首先看一下 gradle.properties

```

Enable configuration caching between builds.

org.gradle.unsafe.configuration-cache=true

android.useAndroidX=true

Non-transitive R classes is recommended and is faster/smaller

android.nonTransitiveRClass=true

Disable build features that are enabled by default,

https://developer.android.com/studio/releases/gradle-plugin#buildFeatures

android.defaults.buildfeatures.buildconfig=false android.defaults.buildfeatures.aidl=false android.defaults.buildfeatures.renderscript=false android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false ```

可以看出,nowinandroid 專案主要做了以下幾個配置

  1. 開啟配置階段快取
  2. 開啟 androidX,並且移除了 Jetifier
  3. 關閉 R 檔案傳遞
  4. 關閉 build features

前面3個配置之前都介紹過,我們來看一下關閉 build features

AGP 4.0.0 引入了一種新方法來控制您要啟用和停用哪些構建功能,如ViewBindingBuildConfig

我們可以在 gradle.properties 中全域性開啟或關閉某些功能,也可以在模組級 build.gradle 檔案中為每個模組設定相應的選項,如下所示:

```

android { // The default value for each feature is shown below. You can change the value to // override the default behavior. buildFeatures { // Determines whether to generate a BuildConfig class. buildConfig = true // Determines whether to support View Binding. // Note that the viewBinding.enabled property is now deprecated. viewBinding = false // Determines whether to support Data Binding. // Note that the dataBinding.enabled property is now deprecated. } } ```

通過停用不需要的構建可能,可以提升我們的構建效能,比如我們最熟悉的BuildConfig,每個模組都會生成這樣一個類,但其實我們在絕大多數情況下是用不到的,因此其實可以將其預設關閉(在 AGP 8.0 中 BuildConfig 生成已經變成預設關閉了)

自動安裝 git hook

有時我們會新增一些 git hook,用於在程式碼提交或者 push 時做一些檢查

但使用 git hook 的一個問題在於,每次拉取新專案之後,都需要手動安裝一下 git hook,這一點常常容易被忘記

那麼有沒有什麼辦法可以自動安裝 git hook 呢?nowinandroid 專案提供了一個示例

``` // settings.gradle.kts

val prePushHook = file(".git/hooks/pre-push") val commitMsgHook = file(".git/hooks/commit-msg") val hooksInstalled = commitMsgHook.exists() && prePushHook.exists() && prePushHook.readBytes().contentEquals(file("tools/pre-push").readBytes())

if (!hooksInstalled) { exec { commandLine("tools/setup.sh") workingDir = rootProject.projectDir } } ```

其實原理很簡單,在settings.gradle.kts中新增以上程式碼,這樣在 Gradle 同步時,就會自動判斷 git hook 有沒有被安裝,如果沒有被安裝則自動安裝

使用 includeBuild 而不是 buildSrc

pluginManagement { includeBuild("build-logic") repositories { google() mavenCentral() gradlePluginPortal() } }

為了支援在不同的模組間共享構建邏輯,此前我們常常會新增一個 buildSrc 模組

但是 buildSrc 模組的問題在於每次發生修改都會導致專案的絕大多數快取失效,從而導致構建速度變得極慢

因此官方現在更推薦我們使用 includeBuild,比如 nowinandroid 的構建邏輯就通過 includeBuild 放在了 build-logic 目錄

如何複用 build.gradle 程式碼?

其實我們專案中的各個模組的 build.gradle 中的程式碼,大部分是重複的,做的都是一些重複的配置,當要修改時就需要一個一個去修改了

nowinandroid 通過抽取重複配置的方式大幅度的減少了 build.gradle 中的程式碼,如下所示

``` plugins { id("nowinandroid.android.feature") id("nowinandroid.android.library.compose") id("nowinandroid.android.library.jacoco") }

android { namespace = "com.google.samples.apps.nowinandroid.feature.author" }

dependencies { implementation(libs.kotlinx.datetime) } ```

這是 nowinandroid 的一個 feature 模組,可以看出除了每個模組不同的namespace與各個模組的依賴之外,其他的內容都抽取到nowinandroid.android.feature等外掛中去了,而這些外掛的程式碼都存放在build-logic 目錄中,通過 includeBuild 引入,大家可自行檢視

總得來說,通過這種方式可以大幅減少重複配置程式碼,當配置需要遷移時也更加方便

使用 Version Catalog 管理依賴

在 build.gradle 中新增依賴有以下幾個痛點

  1. 專案依賴統一管理,在單獨檔案中配置
  2. 不同Module中的依賴版本號統一
  3. 新增依賴時支援程式碼提示

針對這幾種需求,Gradle7.0 推出了一個新的特性,使用 Version Catalog 統一依賴版本,它支援以下特性:

  • 對所有 module 可見,可統一管理所有module的依賴
  • 支援宣告依賴bundles,即總是一起使用的依賴可以組合在一起
  • 支援版本號與依賴名分離,可以在多個依賴間共享版本號
  • 支援在單獨的libs.versions.toml檔案中配置依賴
  • 支援程式碼提示(僅 kts)

noinandroid 中目前已經全面啟用了 Version Catalog,如上所示,統一依賴版本,支援程式碼提示,體驗還是不錯的

關於 Version Catalog 的具體使用可以檢視:【Gradle7.0】依賴統一管理的全新方式,瞭解一下~

程式碼格式檢查

nowinandroid 作為一個開源專案,不可避免地會有第三方貢獻一些程式碼,因此也需要在程式碼合併前做一些格式檢查,保證程式碼風格的統一

nowinandroid 通過 spotless 來檢查程式碼格式,主要是通過兩種方式觸發

  1. 通過上面提到的 git hook,在程式碼 push 時觸發檢查
  2. 通過 github workflow,在程式碼 push 到 main 分支時觸發檢查

上面兩種方式都會呼叫以下命令

./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache --stacktrace

可以看出,這裡主要是執行 spotlessCheck 任務,並且指定了 init-script,我們來看一下 init.gradle.kts 裡面做了什麼

// init.gradle.kts rootProject { subprojects { apply<com.diffplug.gradle.spotless.SpotlessPlugin>() extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> { kotlin { target("**/*.kt") targetExclude("**/build/**/*.kt") ktlint(ktlintVersion).userData(mapOf("android" to "true")) licenseHeaderFile(rootProject.file("spotless/copyright.kt")) } format("kts") { target("**/*.kts") targetExclude("**/build/**/*.kts") // Look for the first line that doesn't have a block comment (assumed to be the license) licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)") } format("xml") { target("**/*.xml") targetExclude("**/build/**/*.xml") // Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml) licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])") } } } }

可以看出,這裡指定了對於 kotlin , kts , xml 等檔案的格式要求,比如 kotlin 程式碼需要遵守 ktlint 規範,並且檔案開頭必須是 license 宣告

自定義 lint 檢查

除了程式碼風格的統一,nowinandroid 專案還自定義了一些 lint 檢查,跟 spoltess 一樣,也是通過 git hook 與 github workflow 兩種方式觸發,兩種方式都會觸發以下程式碼

./gradlew lintDemoDebug --stacktrace

nowinandroid 中有一個自定義的 lint 模組,自定義 lint 規則就定義在這裡,如下所示:

```kotlin class DesignSystemDetector : Detector(), Detector.UastScanner {

override fun createUastHandler(context: JavaContext): UElementHandler {
    return object : UElementHandler() {
        override fun visitCallExpression(node: UCallExpression) {
            val name = node.methodName ?: return
            val preferredName = METHOD_NAMES[name] ?: return
            reportIssue(context, node, name, preferredName)
        }

        override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
            val name = node.receiver.asRenderString()
            val preferredName = RECEIVER_NAMES[name] ?: return
            reportIssue(context, node, name, preferredName)
        }
    }
}

companion object {
    @JvmField
    val ISSUE: Issue = Issue.create(
        id = "DesignSystem",
        briefDescription = "Design system",
        explanation = "This check highlights calls in code that use Compose Material " +
            "composables instead of equivalents from the Now in Android design system " +
            "module."
    )

    // Unfortunately :lint is a Java module and thus can't depend on the :core-designsystem
    // Android module, so we can't use composable function references (eg. ::Button.name)
    // instead of hardcoded names.
    val METHOD_NAMES = mapOf(
        "MaterialTheme" to "NiaTheme",
        "Button" to "NiaFilledButton",
        "OutlinedButton" to "NiaOutlinedButton",
        // ...
    )
    val RECEIVER_NAMES = mapOf(
        "Icons" to "NiaIcons"
    )

    fun reportIssue(
        context: JavaContext, node: UElement, name: String, preferredName: String
    ) {
        context.report(
            ISSUE, node, context.getLocation(node),
            "Using $name instead of $preferredName"
        )
    }
}

} ```

總得來說,這個自定義規則是檢查是否使用了 Compose 的預設 Material 元件而沒有使用 nowinandroid 封裝好的元件,如果檢查不通過則會丟擲異常,提醒開發者修改

總結

本文主要介紹了 nowinandroid 專案構建指令碼中的一系列小技巧,具體包括以下內容

  1. gradle.properties 中的配置
  2. 自動安裝 git hook
  3. 使用 includeBuild 而不是 buildSrc
  4. 如何複用 build.gradle 程式碼?
  5. 使用 Version Catalog 管理依賴
  6. 程式碼格式檢查
  7. 自定義 lint 檢查

希望對你有所幫助~

專案地址

https://github.com/android/nowinandroid