- 移动开发
- 企业应用
【免费下载链接】thunderbird-android
Thunderbird for Android – Open Source Email App for Android (fka K-9 Mail)
本篇指南围绕 Thunderbird for Android(前身 K-9 Mail)仓库中的 docs/architecture/feature-flags.md 展开,系统讲解该项目的特性开关(Feature Flag)体系:如何在 JSON 目录中定义开关、如何通过 Gradle 插件自动生成类型安全的枚举、各类 Provider 如何协作完成求值,以及如何在 Debug 构建的"秘密调试设置"界面中即时启停特性进行验证。读完本文,你将能够在 Thunderbird for Android 的代码库中独立添加、启用、接入并测试一个新的特性开关,并理解其底层求值链路的实现细节。
Thunderbird for Android 秘密调试设置界面中的 Feature Flags 列表
什么是特性开关,什么时候应该添加一个
特性开关(Feature Flag)本质上就是一个简单的布尔值,用来控制一个正在开发中的功能是否对用户可见。有些公司会把特性开关用于 A/B 实验、按时间戳控制发布节奏,或用于其他需要精细管控的场景。Thunderbird for Android 的做法更朴素:主要用于在功能尚未准备好面向广大用户发布之前,把它关掉。
这一做法与团队的协作方式直接相关:项目成员主要在各自 fork 的仓库上工作,很少为持续进行的项目创建特性分支(feature branch)来合并。因此,本地特性开关成为承载"进行中项目"的主要手段——它们只出现在调试构建中,方便团队随时测试正在开发的新功能。
从 docs/architecture/feature-flags.md 的说明可以提炼出添加特性开关的典型判断标准:
- 你要做的功能大到无法用一个 Pull Request 承载;
- 新功能需要和现有代码放在一起进行测试;
- 功能处于开发中、尚未完成、可能破坏应用的状态,需要在面向生产用户开启之前做进一步验证。
特性开关全部写在代码库里(而不是依赖远程服务),因此如果你自己构建一个 Thunderbird 或 K9 的 Debug 版本,就可以自行选择要测试哪些开关。需要注意的是,这些开关背后的功能是不完整的在研功能,贡献者在使用时应当理解这一点。
特性开关架构总览
整个特性开关体系可以分成三层:定义层(JSON 目录 + JSON Schema)、构建层(Gradle 插件代码生成与校验)、运行时层(Provider 求值与 Debug 界面)。
特性开关目录(Feature Flag Catalog)
所有特性开关都集中定义在一个 JSON 目录文件中:config/featureflag/thunderbird_mobile_featureflag.catalog.json。这个文件同时承担两个职责:
- 它是唯一的标志注册表(flag registry):每个开关的 key、默认值、描述、建议晋级时间都记录在这里;
- 它是离线默认值(offline defaults):应用构建时会把这份 JSON 作为 raw resource 打进包里,离线状态下也能求值。
你不需要把目录文件复制进任何 app 模块——项目的构建架构会在工程配置完成后自动引入它。
配套的还有一份 config/featureflag/thunderbird_mobile_featureflag.schema.json,用于校验你在目录里定义的开关是否符合规范(例如 key 必须是小写字母开头的 snake_case、default必须是布尔值、override 里的 key 必须存在于flags数组中)。即使某些需求没有被 Schema 覆盖,FeatureFlagRootPlugin 也会在 Gradle 求值阶段对目录做整体校验,确保定义合法。
重要的特性开关类
虽然日常开发中很少需要改动这些类,但理解它们的职责对排查问题很有帮助。以下是 core/featureflag 模块提供的核心抽象(对应文档中的类清单):
FeatureFlagKey定义开关的 key 与可选描述。它是一个接口,MUST NOT 在:core:featureflag项目之外被实现(测试除外)。FeatureFlagLibraryPlugin 会基于目录自动生成GeneratedFeatureFlagKey枚举类。接口定义见 FeatureFlagKey.kt:
interface FeatureFlagKey { val key: String val description: String? get() = null }FeatureFlagProvider最基础的提供者接口,回答"某个开关是启用、禁用还是不可用"。这是你需要在类里注入(inject)并用来查询开关的接口。它是一个函数式接口(见 FeatureFlagProvider.kt):
fun interface FeatureFlagProvider { fun provide(key: FeatureFlagKey): FeatureFlagResult }典型用法(文档示例):
class MyViewModel : ViewModel() { private val featureFlagProvider: FeatureFlagProvider by inject() fun awesomeGuardedLogic() { if (featureFlagProvider.provide(GeneratedFeatureFlagKey.USE_COMPOSE_FOR_MESSAGE_READER).isEnabled()) { // Do the thing! } } }provide返回的FeatureFlagResult是一个 sealed interface,包含Enabled、Disabled、Unavailable三种状态,并提供了isEnabled()、isDisabled()、isUnavailable()等便捷判断方法(见 FeatureFlagResult.kt)。
CatalogFeatureFlagProvider与BaseCatalogFeatureFlagProvider前者是"基于 JSON 目录的提供者"的契约接口,后者是抽象基类,实现了provide方法的基础逻辑与初始化方法。代码库中所有基于目录的提供者都实现或继承自它们(见 CatalogFeatureFlagProvider.kt)。
DataSourceCatalogFeatureFlagProvider定义了"依赖某个数据源"的提供者如何初始化,是从本地(以及未来可能的远程)拉取 JSON 目录的提供者的基类(见 DataSourceCatalogFeatureFlagProvider.kt)。初始化时会加载目录并解析出当前构建变体对应的 flag 值。
RuntimeDebugOverrideFeatureFlagProvider继承BaseCatalogFeatureFlagProvider,是允许你在可调试(debuggable)应用中覆盖任意开关的提供者。它把当前覆盖值通过 ConfigStore 持久化,因此下次打开应用时你的开关覆盖仍然生效。实现见 RuntimeDebugOverrideFeatureFlagProvider.kt,它对外暴露了setOverride(key, enabled)、clearOverride(key)、clearAllOverrides()三个操作。
BundledCatalogFeatureFlagProvider继承DataSourceCatalogFeatureFlagProvider,负责加载随应用打包的 JSON 目录,同时通过BundledFeatureFlagDefaults接口暴露所有开关的默认值,供 DebugFeatureFlagSectionViewModel 在"恢复默认值"时使用(见 BundledCatalogFeatureFlagProvider.kt)。
MultiFeatureFlagProviderEvaluator用于评估开关解析顺序、产出最终值的提供者。它持有一组CatalogFeatureFlagProvider,在provide被调用时按顺序选择正确的那个;凡是注入FeatureFlagProvider的地方,拿到的实际上就是它的实例。实现见 MultiFeatureFlagProviderEvaluator.kt。
如何添加一个新的特性开关
添加特性开关只需回答三个问题:定义放在哪里、开关如何生成、如何提供给应用。下面逐步说明。
第一步:把开关定义放进 JSON 目录
所有特性开关MUST定义在 config/featureflag/thunderbird_mobile_featureflag.catalog.json 中。打开文件,在flags数组里新增一条定义。文档给出的完整示例:
{ "$schema": "thunderbird_mobile_featureflag.schema.json", "version": "2026-07-30.1", "flags": [ { "key": "my_new_flag", "default": false, "description": "A developer friendly information of what the flag refers to", "time_to_promote": "2030-12-31" } ], "overrides": { "thunderbird": { "debug": { "my_new_flag": true } }, "k9": { "debug": { "my_new_flag": true } } } }必填字段:只有key和default是必填的。但文档强烈建议把其余字段也填上,因为它能给维护者更多关于这个开关用途的上下文。
可选字段:
| 字段 | 作用 |
|---|---|
description | 说明这个开关是关于什么的、启用/禁用会带来什么后果。该描述会显示在 Secret Debug Settings 界面中对应开关 key 的下方 |
time_to_promote | 帮助团队跟踪这个开关应该何时晋级到daily、beta或release;未来计划用该字段在代码库中产生告警——当功能已进入 release 但开关还残留在应用中时,提示移除死代码 |
对照仓库中真实目录 config/featureflag/thunderbird_mobile_featureflag.catalog.json(version 为2026-07-30.1),可以看到实际存在的开关示例:archive_marks_as_read(默认开启)、use_compose_for_message_reader(默认关闭)、use_new_message_reader_css_styles(默认开启并带有time_to_promote: "2026-12-31")等。这些字段与 FlagRegistry.kt 中的@Serializable数据类一一对应(key、default、description、type、time_to_promote)。
记住:每当新增一个开关,都要同步更新version字段。最后,记得在对应的 app 和构建类型中启用它。
第二步:在某个 App / 构建类型中覆盖开关
理想做法是新开关默认保持关闭(如上一节的示例)。但如果你想让它默认在 TfA(Thunderbird for Android)和 K9 的debug构建中开启,只需要把它的 key 加进overrides.<app>.<build_type>并给出想要的值。
以上一节示例为例:my_new_flag默认是false,但 TfA 和 K9 都在debug下把它覆盖为true。这意味着:
- 运行可调试构建时,该开关为启用(Enabled);
- 运行其他构建(TfA Daily、TfA Beta、TfA 正式版、K9-Mail 正式版)时,该开关为禁用(Disabled)。
解析顺序(文档中的 mermaid 流程图):
也就是说:运行时覆盖(Runtime Override,即你在 Secret Debug Settings 里改的值)优先于构建变体覆盖(Catalog Override),构建变体覆盖优先于默认值(Default)。
这个"base + overrides"的合并逻辑在 BaseCatalogFeatureFlagProvider.resolve() 中实现:先以catalog.flags的 default 建立基础映射,再根据当前FeatureFlagContext中的app与build_type属性取出对应的 override 映射做覆盖合并(base + overrides,overrides 胜出)。构建上下文由 FeatureFlagInitializer.kt 中的initializeFeatureFlags()注入,其中包含app、build_type、variant、app_version等属性。
第三步:要不要手动注册到 Provider?
不需要。新的特性开关架构会在工程配置完成后自动映射新 key,并把它包含进GeneratedFeatureFlagKey枚举。这个代码生成由 FeatureFlagLibraryPlugin 完成——它只允许应用在:core:featureflag模块上,并注册"根据 catalog 生成 key 枚举"的 Gradle 任务,再把生成源码接入 KMP 的 source set。
务必注意:更新 catalog 之后,一定要执行一次 Gradle sync,新 key 才会被生成并可供使用。
第四步:确保开关所在的模块依赖了 featureflag
要使用某个开关,功能模块的 Gradle 文件需要包含projects.core.featureflag依赖。文档以消息阅读器为例,展示了 feature/mail/message/reader/api/build.gradle.kts 中真实的依赖配置:
kotlin { ... sourceSets { commonMain.dependencies { ... implementation(projects.core.featureflag) } } }在实际仓库中,该文件确实在commonMain.dependencies中声明了implementation(projects.core.featureflag)。如果你要给其他模块加开关,照此添加即可。
第五步:在代码中访问你的开关
每个构建的特性开关由 Koin 提供。在代码中通过val featureFlagProvider = get<FeatureFlagProvider>()拿到当前构建的FeatureFlagProvider实例,然后用 key 访问开关:
if (featureFlagProvider.provide(GeneratedFeatureFlagKey.USE_COMPOSE_FOR_MESSAGE_READER).isEnabled()) { // Do the thing }也可以把结果先存到变量里再判断:
val composeForMessageReader = featureFlagProvider.provide(GeneratedFeatureFlagKey.USE_COMPOSE_FOR_MESSAGE_READER) ... if (composeForMessageReader.isEnabled()) { // Do the thing }GeneratedFeatureFlagKey是FeatureFlagKey的枚举实现(如USE_COMPOSE_FOR_MESSAGE_READER),由构建插件从 catalog 自动生成,因此千万不要手写枚举条目,一切以 catalog 为准。
运行时求值链路:Provider 是如何协作的
理解"运行时覆盖 > 构建覆盖 > 默认值"背后的实现,能让排查问题事半功倍。整条链路可以从 FeatureFlagModule.kt 中 Koin 模块的装配看出来:
BundledCatalogFeatureFlagProvider以LocalFeatureFlagCatalogDataSource(qualifier 为InjectQualifier.Local)为数据源,从应用 raw 资源thunderbird_mobile_featureflag_catalog读取 JSON(见 LocalFeatureFlagCatalogDataSource.android.kt,它通过applicationContext.resources.openRawResource(...)读取并在 IO 调度器上解析)。RuntimeDebugOverrideFeatureFlagProvider以FeatureFlagConfigStore为持久化后端,把调试覆盖写入 ConfigStore(JSON 编码的 overrides 存储于ConfigKey.StringKey("overrides"),见 FeatureFlagConfigData.kt)。DefaultMultiFeatureFlagProviderEvaluator把上述 provider 列表(当前包含 InMemory 占位与 Local 的 bundled provider;代码中远程 provider 的装配行被注释保留)组合起来,作为最终注入的FeatureFlagProvider。
关键机制在 MultiFeatureFlagProviderEvaluator.kt 的provide()中:它遍历 provider 列表,一旦某个 provider 返回非Unavailable的结果就立即返回(first-match 策略);只有遇到Unavailable时才回落到下一个 provider。这与 BaseCatalogFeatureFlagProvider.provide() 的行为相呼应——当某个 key 不在已解析的目录中时返回Unavailable,从而让求值继续下探。
这一行为的正确性由测试保障:MultiFeatureFlagProviderEvaluatorTest.kt 覆盖了"第一个 provider 返回 Enabled/Disabled 时直接返回"、"第一个返回 Unavailable 时继续询问第二个"等场景。此外还有 RuntimeDebugOverrideFeatureFlagProviderTest.kt 与 BundledCatalogFeatureFlagProviderTest.kt 分别验证覆盖持久化与目录加载逻辑。
确保贡献者知道你的特性开关
按照文档的建议,添加完开关后,最好先为开关本身单独发一个 Pull Request。对于增量式开发的项目,尽量把改动拆小,这样能快速在应用中看到一个可用的开关,然后在此基础上继续堆叠功能。
当你的代码已经放在开关后面时,还要确保后续相关 PR 的审阅者知道要用这个开关来测试功能。这样做的好处是:与同一开关相关的项目在之后更容易被搜索到、也便于在上下文中一起 review。
发 PR 时请给 PR 打上feature-flaglabel(位于 GitHub PR 右侧边栏),并且在 PR 正文中直接提到该开关,例如一行简单的feature flag: your_feature_flag。
在 Debug 构建中查看并测试你的开关
你可能会想:是不是还要把这个开关加进某个列表?完全不用。当你做 Debug 构建时,新开关会立刻出现在应用中,位置就在"Secret Debug Settings Screen"(秘密调试设置界面),由DebugFeatureFlagSectioncomposable 渲染展示(即本文开头截图所示的界面,列表中的开关与 catalog 中的 key 一一对应)。
进入该界面的方式有两种(任选其一):
- 在消息列表界面点击右上角的三点按钮,选择"DEBUG: Feature Flags";
- 打开侧边菜单 → Settings → General Settings → Debugging,点击"Open Secret debug screen"。
在界面中拨动某个开关后,点击右上角 "Apply changes" 应用更改。从 DebugFeatureFlagSectionViewModel.kt 的源码可以看到其交互模型:界面维护defaults(来自BundledFeatureFlagDefaults.defaults(),即 bundled provider 解析出的默认值)与overrides(来自RuntimeDebugOverrideFeatureFlagProvider.overrides流,可实时响应持久化的覆盖变化);切换开关时写入pendingOverrides,点击 Apply 后调用runtimeProvider.setOverride(key, enabled)持久化,并发出RestartMainActivityeffect 让改动生效;"Restore defaults" 则把待定覆盖重置为默认值。
完成这些步骤后,你就可以放心地在 Debug 构建中测试自己的新功能了。最后提醒一句:因为开关覆盖通过 ConfigStore 持久化,下次打开应用时上次的调试选择仍然保留——记得在验证完毕后清理掉不再需要的覆盖。
小结
| 环节 | 关键位置 | 说明 |
|---|---|---|
| 定义开关 | config/featureflag/thunderbird_mobile_featureflag.catalog.json | 唯一注册表,含flags与overrides |
| 校验定义 | config/featureflag/thunderbird_mobile_featureflag.schema.json + FeatureFlagRootPlugin | 构建期校验目录合法性 |
| 生成枚举 | FeatureFlagLibraryPlugin | 生成GeneratedFeatureFlagKey,需 Gradle sync |
| 求值 | MultiFeatureFlagProviderEvaluator.kt | 运行时覆盖 > 构建覆盖 > 默认值 |
| 运行时覆盖 | RuntimeDebugOverrideFeatureFlagProvider.kt | 通过 ConfigStore 持久化调试覆盖 |
| 调试界面 | DebugFeatureFlagSectionViewModel.kt | Secret Debug Settings 界面实时启停开关 |
Thunderbird for Android 的特性开关体系把"定义、校验、代码生成、运行时求值、调试覆盖"串成了一条完整的链路:贡献者只需在 catalog 中新增一条 JSON 记录并执行 Gradle sync,就能立刻在 Debug 构建中通过可视化开关验证新功能,而无需改动任何 Provider 代码。这一设计让大型功能的增量开发与灰度测试变得轻量、可追踪且易于回退。
- 移动开发
- 企业应用
【免费下载链接】thunderbird-android
Thunderbird for Android – Open Source Email App for Android (fka K-9 Mail)
相关推荐
Thunderbird for Android 远程特性开关(Remote Feature Flags)RFC 深度解析:基于 Ktor 的 JSON Catalog 设计
Thunderbird for Android 远程特性开关(Remote Feature Flags)RFC 深度解析:基于 Ktor 的 JSON Cata
移动开发企业应用Thunderbird for Android 特性开关声明式目录(Declarative Feature Flag Catalog)技术设计解析
Thunderbird for Android 特性开关声明式目录(Declarative Feature Flag Catalog)技术设计解析 导读 本文基
移动开发企业应用在 Carbon 中使用 @carbon/feature-flags:运行时与 Sass 特性开关完全指南
在 Carbon 中使用 @carbon/feature flags:运行时与 Sass 特性开关完全指南 @carbon/feature flags 是 IB
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考