引言
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() extensions.configure { 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 (