⚠️本文为稀土技能社区首发签约文章,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)

学习一下 nowinandroid 的构建脚本

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 规矩就定义在这儿,如下所示:

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 查看

希望对你有所帮助~

项目地址

github.com/android/now…