學習一下 nowinandroid 的構建指令碼
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 專案主要做了以下幾個配置
- 開啟配置階段快取
- 開啟
androidX
,並且移除了Jetifier
- 關閉
R
檔案傳遞 - 關閉
build features
前面3個配置之前都介紹過,我們來看一下關閉 build features
AGP 4.0.0 引入了一種新方法來控制您要啟用和停用哪些構建功能,如ViewBinding
,BuildConfig
。
我們可以在 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 中新增依賴有以下幾個痛點
- 專案依賴統一管理,在單獨檔案中配置
- 不同Module中的依賴版本號統一
- 新增依賴時支援程式碼提示
針對這幾種需求,Gradle7.0 推出了一個新的特性,使用 Version Catalog 統一依賴版本,它支援以下特性:
- 對所有 module 可見,可統一管理所有module的依賴
- 支援宣告依賴bundles,即總是一起使用的依賴可以組合在一起
- 支援版本號與依賴名分離,可以在多個依賴間共享版本號
- 支援在單獨的libs.versions.toml檔案中配置依賴
- 支援程式碼提示(僅 kts)
noinandroid 中目前已經全面啟用了 Version Catalog,如上所示,統一依賴版本,支援程式碼提示,體驗還是不錯的
關於 Version Catalog 的具體使用可以檢視:【Gradle7.0】依賴統一管理的全新方式,瞭解一下~
程式碼格式檢查
nowinandroid 作為一個開源專案,不可避免地會有第三方貢獻一些程式碼,因此也需要在程式碼合併前做一些格式檢查,保證程式碼風格的統一
nowinandroid 通過 spotless 來檢查程式碼格式,主要是通過兩種方式觸發
- 通過上面提到的 git hook,在程式碼 push 時觸發檢查
- 通過 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 專案構建指令碼中的一系列小技巧,具體包括以下內容
- gradle.properties 中的配置
- 自動安裝 git hook
- 使用 includeBuild 而不是 buildSrc
- 如何複用 build.gradle 程式碼?
- 使用 Version Catalog 管理依賴
- 程式碼格式檢查
- 自定義 lint 檢查
希望對你有所幫助~
專案地址
- kotlin-android-extensions 外掛到底是怎麼實現的?
- 江同學的 2022 年終總結,請查收~
- kotlin-android-extensions 外掛將被正式移除,如何無縫遷移?
- 學習一下 nowinandroid 的構建指令碼
- Kotlin 預設可見性為 public,是不是一個好的設計?
- 2022年編譯加速的8個實用技巧
- 落地 Kotlin 程式碼規範,DeteKt 瞭解一下~
- Gradle 進階(二):如何優化 Task 的效能?
- 開發一個支援跨平臺的 Kotlin 編譯器外掛
- 開發你的第一個 Kotlin 編譯器外掛
- Kotlin 增量編譯是怎麼實現的?
- Gradle 都做了哪些快取?
- K2 編譯器是什麼?世界第二高峰又是哪座?
- Android 效能優化之 R 檔案優化詳解
- Kotlin 快速編譯背後的黑科技,瞭解一下~
- 別了 KAPT , 使用 KSP 快速實現 ButterKnife
- Android Apk 編譯打包流程,瞭解一下~
- 如何優雅地擴充套件 AGP 外掛
- ASM 插樁採集方法入參,出參及耗時資訊
- Transform 被廢棄,ASM 如何適配?