项目级build.gradle配置:allprojects、subprojects、ext扩展属性

模块化项目里,根目录下的build.gradle就像指挥官。它统筹着所有子模块的编译配置。我见过不少团队把依赖版本写死在各个子模块里,结果升级时要改几十个文件……嗯,今天就来聊聊怎么用好allprojects、subprojects和ext,让配置既清晰又好维护。

allprojects与subprojects——全局与子模块的配置边界

这两个块看起来很像,但作用范围有本质区别。我用一个表格来对比,你一看就明白:

块 作用对象 典型用途
allprojects {} 根项目 + 所有子模块 全局仓库地址、通用插件、全局变量
subprojects {} 仅子模块 模块公共配置(如Java版本、lint选项)

看明白了吧?allprojects连根项目自己都包括,而subprojects只针对子模块。我建议仓库声明放在allprojects里,模块通用的编译配置放在subprojects里。举个实际例子:

// 项目级 build.gradle
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

subprojects {
    // 每个子模块都应用 common.gradle 插件约定
    afterEvaluate { project ->
        if (project.plugins.hasPlugin('com.android.application') ||
            project.plugins.hasPlugin('com.android.library')) {
            android {
                compileSdkVersion 34
                defaultConfig {
                    minSdkVersion 21
                    targetSdkVersion 34
                }
            }
        }
    }
}

我在项目里曾遇到一个坑:有人在subprojects里配置了apply plugin: 'kotlin-android',结果导致某个纯Java模块也被强制应用了Kotlin插件,编译时报了一堆class冲突……所以我后来改用按条件判断,就像上面代码里那样——先检查插件再配置。原则是:不要无脑套用,要区分模块类型。

ext扩展属性——统一版本管理的神器

说到版本管理,ext是模块化项目的救星。你想想,几十个模块各自维护targetSdk、versionName,那画面太美我不敢看。我习惯在项目根build.gradle底部定义一个ext块,把所有公共版本号收拢起来:

ext {
    // 版本号
    compileSdkVersion = 34
    minSdkVersion = 21
    targetSdkVersion = 34

    // 依赖版本
    kotlinVersion = '1.9.22'
    composeBomVersion = '2024.02.00'
    retrofitVersion = '2.9.0'
    okhttpVersion = '4.12.0'

    // 依赖简写(避免到处拷贝坐标)
    libs = [
        kotlinStdlib : "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion",
        composeUi    : "androidx.compose.ui:ui:1.6.0",
        retrofit     : "com.squareup.retrofit2:retrofit:$retrofitVersion",
        okhttp       : "com.squareup.okhttp3:okhttp:$okhttpVersion"
    ]
}

小技巧:用ext.libs这种嵌套结构来定义完整的依赖坐标,子模块引用时只需写implementation rootProject.ext.libs.retrofit,既统一又简洁。不过要注意:ext里的变量是动态的,在buildscript块里无法直接使用——如果要在buildscript里引用版本号,需要把版本号定义在项目根目录的gradle.properties里。

配置优先级与执行顺序

这三个块在同一个build.gradle里执行顺序很关键:先执行buildscript {}和插件声明,然后按文件书写顺序执行allprojects、subprojects、ext。但ext其实是在allprojects之前就被解析的?我亲自验证过:ext块会在配置阶段就赋值,所以allprojects里能直接读取ext定义的变量。而subprojects里也能读取,但要注意闭包的延迟求值问题。

我曾经遇到一个诡异的bug:在subprojects里写println ext.kotlinVersion,发现输出的是null。后来才意识到是闭包捕获了变量引用,而当时ext还没赋值完毕。解决方案就是不要在subprojects的顶层闭包内直接引用ext变量,而是要放在afterEvaluate里,或者使用rootProject.ext.kotlinVersion显式引用——你可以试试看。

⚠ 特别注意:不要在allprojects里修改子模块的android块(如compileSdkVersion)!我见过有人想省事,在allprojects里统一设置android.compileSdkVersion = 34,结果某些模块的android块还没创建,直接报了MissingPropertyException。正确的做法是在subprojects里用afterEvaluate包装,或者让子模块显式继承一个公共gradle脚本。

整体关系图

下面这张图帮你理清allprojects、subprojects、ext在项目级配置中的角色和关系:

项目级 build.gradle 配置层次 根 build.gradle allprojects {} subprojects {} ext {} repositories: google, mavenCentral apply common plugins 公共 compileSdk / minSdk afterEvaluate 条件判断 lintOptions / testOptions 版本号常量定义 依赖坐标简写 libs 子模块通过 rootProject.ext 引用 执行顺序:ext 先于 allprojects 与 subprojects 解析 subprojects 内引用 ext 变量建议使用 rootProject.ext.xxx

你看,ext像是一个全局常量池,allprojects和subprojects都能直接食用它。而allprojects负责把通用仓库声明透传给所有项目,subprojects则专注给子模块叠加公共编译配置。这三者配合好了,模块化项目的配置就不再是“一个改、处处改”的噩梦。我自己现在的模板项目,只要改一行ext里的版本号,整个项目的依赖就能自动同步,舒心得很。