Compose Multiplatform 如何配置多个 flavor 并切换品牌资源?
结论
应通过 compose.resources { customDirectory(...) } 将选中的品牌资源目录关联到 commonMain。仅调用 Kotlin source set 的 resources.srcDirs(...),无法确认该目录已接入 Compose 的资源生成流程,也不能获得 Android flavor 的资源覆盖规则。Compose 资源配置文档
对于约 8 个品牌的白标应用,可以先在每次 Gradle 构建时选择一个品牌:Android 和 iOS 使用同一个 appTarget 参数,选择对应的共享资源和差异代码。
AGP 9 并没有全面取消 flavors。com.android.kotlin.multiplatform.library 插件采用单一 variant,没有 product flavors 和 build types;独立 Android 应用模块仍可配置 flavors。Android-KMP 插件说明
先检查原配置中的两个问题
原代码定义了 appTargetDir,条件判断时却使用 envDir。如果实际代码也是这样,应检查目录变量是否写错;如果 envDir 未定义,脚本会直接编译失败。
还需要检查路径。src/targets/$appTarget 相对于 shared 模块目录解析,因此实际目录应为:
shared/src/targets/myTarget/
题目中的目录树没有展示 src,需要确认是展示时省略了这一层,还是实际路径与脚本不一致。
这些是片段中可见的疑点。由于没有完整构建脚本和插件版本,不能仅凭这些内容断言唯一根因。
用 customDirectory 显式选择品牌资源
先让每个品牌目录包含构建所需的完整资源,避免同时引入公共资源回退和 XML 合并逻辑。建议使用以下结构:
shared/
└── src/
├── commonMain/
│ └── kotlin/
└── targets/
├── myTarget/
│ ├── kotlin/
│ └── composeResources/
│ └── values/
│ └── strings.xml
└── anotherTarget/
├── kotlin/
└── composeResources/
└── values/
└── strings.xml
将下面的配置放入 shared/build.gradle.kts,与现有 target 配置合并。模块需要已应用 Kotlin Multiplatform 和 org.jetbrains.compose,且 Compose 插件需要支持 customDirectory:
val appTarget = providers.gradleProperty("appTarget").orNull
?: throw GradleException("Missing -PappTarget=<brand>")
val targetDir = layout.projectDirectory.dir("src/targets/$appTarget")
val brandResourcesDir = targetDir.dir("composeResources")
check(brandResourcesDir.asFile.isDirectory) {
"Missing brand resources: ${brandResourcesDir.asFile}"
}
kotlin {
sourceSets {
named("commonMain") {
kotlin.srcDir(targetDir.dir("kotlin").asFile)
dependencies {
implementation(compose.components.resources)
}
}
}
}
compose.resources {
customDirectory(
sourceSetName = "commonMain",
directoryProvider = provider { brandResourcesDir }
)
}
strings.xml 应放在 composeResources/values/ 下,例如:
<resources>
<string name="app_name">My Target</string>
</resources>
构建时传入品牌参数,例如:
./gradlew :androidApp:assembleDebug -PappTarget=myTarget
这里假设 Android 应用模块有 assembleDebug 任务;如果应用模块配置了 flavors,应换成对应的 assemble 任务。
Compose 页面继续使用该 shared 模块生成的 Res,例如 stringResource(Res.string.app_name)。检查导入的 Res 和字符串访问器是否来自预期模块,并在修改后重新构建,以更新生成代码。资源访问文档
公共资源与品牌差异如何合并?
customDirectory 用于关联资源目录,不应当作 Android flavor overlay 的替代实现。对于同一个 source set,也不要通过多次调用它来表达目录覆盖顺序;插件源码使用 source set 名称保存对应的目录配置。Resources DSL 源码
确认品牌切换正常后,如果希望维护“公共资源 + 品牌差异”,可以增加一个 Gradle 资源生成任务:
- 读取公共资源和当前品牌资源。
- 按明确规则生成一份完整资源树,例如输出到
build/generated/brandResources/myTarget。 - 将任务输出通过
directoryProvider接入customDirectory,使资源处理任务依赖该生成任务。
文件覆盖和字符串合并需要区别处理。如果直接用品牌 values/strings.xml 覆盖公共同名文件,公共文件中未在品牌文件内重复定义的字符串就会丢失。如果需要逐项覆盖,应解析 XML,并按资源类型、name 和限定目录合并;plurals、string-array 等还需要各自的处理规则。
资源数量不大时,先为每个品牌维护完整的资源目录,通常更容易验证和排错。
差异代码与平台配置怎么组织?
共享 Kotlin 代码可以保留在 commonMain/kotlin,品牌实现放在选中的 targets/<brand>/kotlin。这些目录会共同参与编译,因此不要在公共目录与品牌目录中重复声明同一个类或函数。颜色配置可以通过共享接口或配置对象传入 UI,每次构建只包含一套品牌实现。
加入 commonMain 的品牌代码必须能在 Android 和 iOS 上编译。Android 专用代码应放入 Android source set,iOS 专用代码应放入相应 iOS source set。
Android 的 applicationId、启动图标、Manifest 配置,以及 iOS 的 Bundle Identifier、签名和原生资源,仍应由各自的应用工程管理。它们可以与共享模块采用相同的品牌映射,但不能只靠 Compose 字符串目录完成切换。
使用 Xcode 构建 iOS 时,也必须让它调用的 Gradle 构建收到相同的 appTarget。Android flavor 名称和 Xcode scheme 不会因为名字相同就自动变成这个 Gradle 属性。
验证时注意什么?
先选两个品牌,给 app_name 设置明显不同的值,分别构建并运行 Android 和 iOS,确认共享页面显示正确的字符串。
同时检查构建参数、实际资源路径、生成的访问器及最终打包内容。目录存在或脚本能够解析,只能说明配置满足部分结构条件,不能证明资源已正确生成和打包。
该方案在一次 Gradle 配置中只选择一个品牌。如果需要并行产出多个品牌,应使用独立的构建工作目录,避免不同构建写入同一份生成资源和输出目录。
备注:内容仅供参考。
