用途
Android 生成的配置对象在 common 代码中并不存在。BuildKonfig 用来填补这一空缺:它根据构建脚本提供的值生成一个对象,该对象在 commonMain 中可见,从而让多平台应用在构建期获得统一配置。
主要能力
在 buildkonfig 块中指定包名、暴露的对象名以及默认配置字段,例如:
buildkonfig {
packageName = "com.example.app"
exposeObjectWithName = "BuildKonfig"
defaultConfigs {
buildConfigField(STRING, "versionName", libs.versions.version.name.get())
buildConfigField(INT, "versionCode", libs.versions.version.code.get())
// …plus the secret-bearing fields below
}
}
生成结果是你所指定包下的一个普通 public 对象,成员为 public val,因此每个平台、每个源集都以相同方式读取它——不需要 expect/actual,也不需要平台访问器。
密钥与两个分支
密钥来自仓库根目录下一个未被跟踪的 properties 文件(已被 git 忽略;CI 在构建前用仓库 secret 写入该文件)。构建由 Gradle 属性选择两个分支:一个携带付费集成凭据,一个不携带。
// adapted — field names genericised
val isFullBuild: Boolean = try { extra["isFullBuild"] == "true" } catch (e: Exception) { false }
if (isFullBuild) {
try {
val properties = Properties()
properties.load(rootProject.file("local.properties").inputStream())
buildConfigField(STRING, "integrationApiKey", properties.getProperty("INTEGRATION_API_KEY") ?: "")
buildConfigField(STRING, "integrationSecret", properties.getProperty("INTEGRATION_SECRET") ?: "")
buildConfigField(STRING, "crashReportingDsn", properties.getProperty("CRASH_DSN") ?: "")
} catch (e: Exception) {
println("Failed to load secrets from local.properties: ${e.message}")
buildConfigField(STRING, "integrationApiKey", "")
buildConfigField(STRING, "integrationSecret", "")
buildConfigField(STRING, "crashReportingDsn", "")
}
} else {
// The no-secrets branch ships no credentials, so the availability check stays false
// and the feature hides itself. The stub module is linked in this flavour anyway.
buildConfigField(STRING, "integrationApiKey", "")
buildConfigField(STRING, "integrationSecret", "")
buildConfigField(STRING, "crashReportingDsn", "")
}
CI 在调用 Gradle 之前提供该文件,每个密钥一行:
- run: |
echo 'CRASH_DSN=${{ secrets.CRASH_DSN }}' > ./local.properties
echo 'INTEGRATION_API_KEY=${{ secrets.INTEGRATION_API_KEY }}' >> ./local.properties
使用方式与注意事项
用空字符串,而不是缺失字段。 两个分支必须声明相同的字段名。只在其中一个分支定义的字段,会在 common 代码中对另一个分支造成编译错误,于是无密钥分支就必须成为不同的源集——而这正是你想避免的结构。空值可以维持单一代码库。
让空白在运行时表示“不可用”,只在一处、用命名检查完成。 这样禁用路径就是所有调用方都已调用的一个函数:
fun isIntegrationAvailable(): Boolean = apiKey.isNotEmpty() && sharedSecret.isNotEmpty()
每个入口点都在该检查上提前返回,设置界面隐藏整个区块。这样,在没有凭据的构建中,付费集成分支与桩分支行为一致,没有密钥的贡献者得到的是可用的应用,而不是坏掉的功能。
捕获并继续,不要让缺失文件导致构建失败。 全新克隆没有 properties 文件。在 try 中加载它,catch 写入同样的空字段,贡献者的首次构建就能成功——这就是“必须是员工才能编译”与“必须是员工才能使用这一个功能”的区别。
永远不要从生成文件中读取值。 build 目录下生成的源码保存着上一次本地构建注入的内容,包括凭据。必要时可以看它的结构;值要从构建脚本获取。
按值而不是按 flavour 保护消费方。 只在字段非空时初始化可选服务——if (BuildKonfig.crashReportingDsn.isNotEmpty()) { … }。flavour 检查能编译通过,但一旦有人在没有密钥文件的情况下构建完整 flavour 就会出问题。
较新的 Gradle 会拒绝生成任务与其消费方之间的隐式依赖。 在 Gradle 9 的严格任务依赖校验下,消费生成源码却未声明依赖的任务会直接构建失败,而不是警告。如果插件尚未跟进,可以按名称自行接线:
afterEvaluate {
tasks.matching { it.name.startsWith("prepare") && it.name.endsWith("ArtProfile") }
.configureEach { dependsOn("generateBuildKonfig") }
}
错误会同时点名两个任务,但失败的是消费方,因此信息读起来像是关于你从未碰过的子系统。按名称模式匹配,而不是列出任务:任务集会随构建类型和 flavour 变化,硬编码列表会在下一个变体加入时失效。
版本值应放在版本目录中,而不是这个块里。 从版本目录读取(libs.versions.version.name.get())可以让一个同时出现在打包配置和发布工作流中的值保持单一来源。
验证
# the untracked file must be ignored AND actually untracked
git check-ignore -v local.properties
git ls-files --error-unmatch local.properties # must report "did not match any file"
# what the current build injected — SHAPE ONLY, values elided so no secret hits the terminal
find <app-module>/build/generated/source/buildkonfig -name 'BuildKonfig.kt' \
-exec sed 's/=.*/= <elided>/' {} \;
# nothing secret should ever appear in a tracked file
git grep -nI -e 'API_KEY' -e 'SECRET' -e 'DSN' -- '*.kts' '*.kt' '*.yml'
最后一条命令应当只命中名称——属性键和 CI 中的 secret 引用。命中值意味着它已经被提交,修复方式是轮换凭据,而不是删除该行。
适用场景与限制
适用于需要在 Kotlin Multiplatform 的 common 代码中统一读取构建期配置、并希望区分“含付费集成凭据”与“不含凭据”两种构建的项目。限制与前提包括:密钥文件必须被 git 忽略且确实未被跟踪;两个分支字段名必须一致;需要处理 Gradle 9 严格任务依赖校验;生成文件中的值不可作为读取来源。