用途
该技能用于解决“同一套代码需要发布两种构建形态”的问题:一种是带有专有或需凭证集成的完整构建,另一种是不含专有产物的开源构建。它通过“孪生模块”模式让调用方完全感知不到两者的差异。
主要能力与做法
孪生模块结构
<feature>模块封装真实集成;<feature>-empty模块返回让所有调用方走“不可用”路径的值,但不让调用方显式分支;- 两个模块声明相同的 package 和相同的 Android namespace,因此 import 不变,链接到哪个模块对调用点不可见。
桩实现原则
桩需要与真实模块具有相同的可编译签名,但不含任何厂商产物。桩在真实模块做转换的地方应返回恒等值(如 wrapPlayer 原样返回 localPlayer),而不是 null,以免迫使调用方编写分支。可用性检查是整套隐藏机制的核心:
fun is<Feature>Available(): Boolean = apiKey.isNotEmpty() && sharedSecret.isNotEmpty()
桩版本硬编码返回 false。这样,即使完整构建未注入凭证,其行为也与桩构建一致,而不会出现永远无法成功的登录入口。
以 Gradle 属性而非 product flavors 选择
每个消费模块在构建脚本顶部读取同一属性:
val isFullBuild: Boolean = try { extra["isFullBuild"] == "true" } catch (e: Exception) { false }
if (isFullBuild) implementation(projects.<feature>) else implementation(projects.<feature>Empty)
资料解释了不用 product flavors 的原因:flavors 是 Android Gradle Plugin 的概念,而 Kotlin Multiplatform 模块的 jvm()、ios*() 与 common source sets 没有 flavor 维度,无法为其选择依赖;且孪生模块会被仅 Android 模块和 multiplatform 模块的 commonMain 同时消费,单一属性可覆盖所有模块与目标。
CI 使用方式
属性在 gradle.properties 中有默认值;持续集成在调用 Gradle 前向同一文件追加一行,每个作业一个值:
echo "isFullBuild=false" >> ./gradle.properties
易错点(资料列举)
- 每个消费模块必须读取同一属性,否则依赖图会混用;某个模块硬编码真实孪生模块会把专有代码经由传递路径带入开源构建,且链接正常、不会报错。资料提供了 grep 命令枚举两侧并比对。
- 两个孪生模块必须声明相同的 package 与 Android namespace,交换才不可见。
- 桩在真实模块做转换处返回恒等值而非 null;返回 null 会迫使每个调用方分支,违背该模式的目的。
- 需确认可用性检查被实际调用:grep 应至少命中设置界面与服务入口。
- 桩的依赖需足以编译相同签名,且不含厂商产物;比对依赖时注意 multiplatform 模块的声明缩进在 sourceSets 内,简单
/^dependencies {/匹配会漏掉,导致 diff 静默通过。资料给出了deps()函数与 diff 用法。 - 两个文件靠手工保持同步,只在真实模块新增而桩遗漏的函数会在本地编译通过、在其他分支失败;data class、sealed 层级与常量同样计入,桩需重新声明完整结果层级(含错误码)。
- 读取属性时缺失不能报错,try/catch 默认 false 是刻意设计:范围内无属性的模块得到桩(永远可构建的分支)。签入的默认值可以是完整构建,但这仅在凭证读取器捕获缺失 local.properties 并写入空字段、再由可用性检查转回桩行为时才安全。
- 桩不是放 TODO 的地方:这些函数从播放回调与 composition 调用,必须完整且在热路径上安全;每次调用一行日志可以,抛异常或返回部分初始化对象不行。
验证方式
资料给出两条验证命令:
- 比对两侧公开声明名称,末尾的
sort不可省略(uniq -u只合并相邻重复项),输出必须为空:
for m in <feature> <feature>-empty; do
grep -rhoE '^(fun|suspend fun|data class|sealed interface|@Composable)[^({]*' "$m/src" | sort
done | sort | uniq -u
- 两个孪生模块无条件保留在项目列表中,选择发生在每个依赖处;条件式 include() 会让整个配置依赖该属性,破坏不解析项目列表的工具:
grep -n '<feature>' settings.gradle.kts
适用场景
需要在同一代码库中同时产出含专有集成与不含专有产物的两种构建,尤其是 Kotlin Multiplatform 项目中跨 Android 与多平台目标统一选择依赖时。
限制
资料未提供该技能的具体运行环境要求、依赖版本、许可证或收费信息,相关事实暂未提供。