PAYATHON 2026

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 资源生成任务:

  1. 读取公共资源和当前品牌资源。
  2. 按明确规则生成一份完整资源树,例如输出到 build/generated/brandResources/myTarget。
  3. 将任务输出通过 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 配置中只选择一个品牌。如果需要并行产出多个品牌,应使用独立的构建工作目录,避免不同构建写入同一份生成资源和输出目录。

备注:内容仅供参考。