尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flutter 特性开关实战指南:在 flutter 工具中新增、配置与消费 Feature Flag(源码级解析)

Flutter 特性开关实战指南:在 flutter 工具中新增、配置与消费 Feature Flag(源码级解析) Flutter 特性开关实战指南在 flutter 工具中新增、配置与消费 Feature Flag源码级解析【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter本文基于 Flutter 仓库贡献文档docs/contributing/Feature-flags.md与当前仓库源码编写系统讲解 Flutter 特性开关feature flag的完整生命周期如何在flutter_tools中声明一个开关、如何让它可在master/beta/stable各渠道启用或默认开启、其取值的优先级规则、在工具层与框架层的消费方式以及工具单测、集成测试与框架单测中如何正确启用开关。读完后你可以独立完成一个新特性开关的注册、灰度发布与最终移除并理解FLUTTER_ENABLED_FEATURE_FLAGS等运行时机制的底层实现。特性开关是什么概念与适用场景Flutter 工具flutter支持特性开关feature flag这一概念——一组布尔型开关用于告知、改变、允许或拒绝访问某些行为作用对象可以是工具本身也可以是框架package:flutter及相关包。一个典型用法是启用 Swift Package Managerflutter config --enable-swift-package-manager特性开关可以在多个层级被配置全局machine 级对整个机器生效通过flutter config写入全局配置文件本地app 级对特定 Flutter 工程生效通过pubspec.yaml的config:段声明单次测试在单元测试中注入按发布渠道自动生效master、beta、stable各自可以有不同的默认值。官方为这些能力提供的价值是有条件地、一致地、便捷地改变行为典型场景包括场景说明灰度发布Gradual rollouts将新特性先开放给一小部分用户A/B 测试方便对比不同实现的效果紧急开关Kill Switches不改大量代码即可快速关闭出问题的特性实验性访问Experimental access让未准备好全面开放的功能可被显式启用需要特别注意一条仓库文档明确给出的边界声明修改或移除实验性开关、或修改受实验性开关保护的行为不被视为 breaking change受开关保护的 API 随时可能变化。开关的声明Feature类与渠道设置所有工具侧开关统一注册在 packages/flutter_tools/lib/src/features.dart。每个开关是一个顶层const Feature例如 Web 支持与 HCPP 平台视图渲染模式的真实定义/// The [Feature] for flutter web. const flutterWebFeature Feature.fullyEnabled( name: Flutter for web, configSetting: enable-web, environmentOverride: FLUTTER_WEB, ); /// Whether the HCPP (Hybrid Composition) platform view rendering mode is used /// by default on Android. const hcpp Feature( name: the HCPP platform view rendering mode, configSetting: enable-hcpp, environmentOverride: FLUTTER_ENABLE_HCPP, master: FeatureChannelSetting(available: true, enabledByDefault: true), beta: FeatureChannelSetting(available: true, enabledByDefault: true), stable: FeatureChannelSetting(available: true), );从 Feature 类定义 看各参数的含义与作用如下参数作用name必填面向用户的特性描述会出现在flutter config的帮助文本里configSetting可选。提供后该开关才能通过flutter config --name或pubspec.yaml的config:段配置不提供则只能用于单元测试runtimeId可选。提供后框架运行时的已启用开关集合debugEnabledFeatureFlags中会包含这个值用于框架层消费开关environmentOverride可选。指定可覆盖开关的环境变量名值需为true大小写不敏感文档明确这是面向 CI 的用法不是对外宣传的启用方式extraHelpText可选。追加到flutter config帮助信息末尾的补充说明warningMessageOnDisable可选。开关被显式关闭时打印的警告如 swiftPackageManager 定义了未来版本将强制启用 SPM的警告master/beta/stable各渠道的FeatureChannelSetting见下FeatureChannelSetting只有两个布尔字段默认值均为 falsefinal class FeatureChannelSetting { const FeatureChannelSetting({this.available false, this.enabledByDefault false}); /// 该渠道上特性是否可用。false 表示即使有任何配置也无法启用。 final bool available; /// 该渠道上是否默认启用。 final bool enabledByDefault; }也就是说新注册的开关默认在三个渠道上都不可用这正是文档所说新增后只能在自己的单元测试里启用的底层原因。此外还有一个便捷构造器Feature.fullyEnabled(...)等价于三个渠道全部available: true, enabledByDefault: true即该开关已毕业、全面默认开启。当前仓库中所有开关集中在FeatureFlags.allFeatures列表中features.dart L103-L126包括 Web、Linux/macOS/Windows 桌面、Android、iOS、Fuchsia、自定义设备、CLI 动画、native assets、Dart data assets、record use 实验、Swift Package Manager、UIScene 迁移、riscv64、macOS arm64-only、HCPP、工具扩展等。其中带runtimeId的如windowing、accessibility_evaluations会额外打通到框架运行时。新增一个特性开关完整步骤以下流程继承自文档并对照当前源码核实。以文档中的独角兽表情unicorn emojis示例为例。第 1 步添加顶层const Featureconst Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, );如需让开关在单元测试之外可配置需补充相应参数允许flutter config或pubspec.yaml的config:段配置 → 加configSettingconst Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, configSetting: enable-unicorn-emojis, );允许框架运行时消费 → 加runtimeIdconst Feature unicornEmojis2 Feature( name: add unicorn emojis in lots of fun places, runtimeId: enable-unicorn-emojis, );允许环境变量覆盖 → 加environmentOverrideconst Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, environmentOverride: FLUTTER_UNICORN_EMOJIS, );第 2 步在抽象类FeatureFlags中新增 getterFeatureFlags是开关查询的抽象接口features.dart L25文档源码注释明确指出它在 google3 中被扩展——每当新增一个开关google3 一侧也需要同步更新因此贡献文档要求创建 G3Fix 以更新 google3 的Google3Features给Google3Features加同名 getter并把新 Feature 加入其allFeatures。这是内部构建的配套步骤开源仓库中的对应实现见下一步的FlutterFeatureFlagsIsEnabled。第 3 步在FlutterFeatureFlagsIsEnabled中实现同名 getter该 mixin 位于 packages/flutter_tools/lib/src/flutter_features.dart每个 getter 都是对isEnabled(...)的薄封装mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags { override bool get isUnicornEmojisEnabled isEnabled(unicornEmojis); }第 4 步把新开关加入FeatureFlags.allFeaturesListFeature get allFeatures const Feature[ // ... unicornEmojis, ];只有进入allFeatures的开关才会被allConfigurableFeatures驱动flutter config帮助与校验、allEnabledFeatures以及运行时 dart-define 注入逻辑遍历到。让开关可用再到默认开启渠道级灰度新增开关后它默认不可用disabled 且无法在测试外启用这允许开发者在本地自由迭代而无需处理用户的现场问题。之后可按渠道逐步放开阶段一在某渠道可用但默认关闭const Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, configSetting: enable-unicorn-emojis, master: FeatureChannelSetting(available: true), );要所有渠道可用则三个渠道都设available: true。阶段二按渠道默认开启例如只在master默认开启const Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, configSetting: enable-unicorn-emojis, master: FeatureChannelSetting(available: true, enabledByDefault: true), beta: FeatureChannelSetting(available: true), stable: FeatureChannelSetting(available: true), );当前仓库的 hcpp 开关 正是这种形态的实例master/beta默认开启stable仅可用riscv64 也是 master 默认开、其余渠道仅可用。阶段三全渠道默认开启const Feature unicornEmojis Feature.fullyEnabled( name: add unicorn emojis in lots of fun places, configSetting: enable-unicorn-emojis, );仓库中flutterWebFeature、flutterAndroidFeature、swiftPackageManager配合未来将强制启用的关闭警告等均已处于该阶段。开关取值的解析优先级Precedence以同时声明了configSetting与environmentOverride的开关为例const Feature unicornEmojis Feature( name: add unicorn emojis in lots of fun places, configSetting: enable-unicorn-emojis, environmentOverride: FLUTTER_ENABLE_UNICORN_EMOJIS, );Flutter 采用的优先级顺序文档与源码一致应用的pubspec.yamlconfig:段flutter: config: enable-unicorn-emojis: true工具全局配置flutter config写入的、位于用户主目录下的平台相关配置文件flutter config --enable-unicorn-emojis环境变量FLUTTER_ENABLE_UNICORN_EMOJIStrue flutter some-command以上都未设置时回退到当前渠道的默认值enabledByDefault。该顺序在源码中可完整验证。FlutterFeaturesConfig.isEnabled 的实现是config 值 ?? 环境变量值其中 config 值又按项目级 ?? 全局级解析_isEnabledAtProjectLevel ?? _isEnabledByGlobalConfig环境变量匹配规则为小写后等于true才为真。而最外层 FlutterFeatureFlags.isEnabled 的逻辑是final FeatureChannelSetting featureSetting feature.getSettingForChannel(currentChannel); // 渠道不可用任何配置都无法启用 if (!featureSetting.available) { return false; } return _featuresConfig.isEnabled(feature) ?? featureSetting.enabledByDefault;两点值得注意渠道available: false是第一道硬门槛优先级高于一切配置来源配置值解析有严格的类型校验pubspec.yaml的config段必须是 map其中的开关值必须是布尔值否则直接throwToolExit见 flutter_features_config.dart L130-L167。此外还有一个特例isCliAnimationEnabled 在TERMdumb时强制返回 false即使开关本身是启用的。在工具中消费开关注入式与全局式在flutter_tools内部读取开关有两种方式。推荐方式显式注入FeatureFlags实例。对较大特性及其测试直接在构造函数中接收一个FeatureFlags引用例如文档给出的WebDevices示例class WebDevices extends PollingDeviceDiscovery { // 虽然可以从全局作用域见下文注入但直接持有一个 FeatureFlags // 实例引用能让这个较大特性及其测试更加显式。 WebDevices({required FeatureFlags featureFlags}) : _featureFlags featureFlags; final FeatureFlags _featureFlags; override FutureListDevice pollingGetDevices({Duration? timeout}) async { if (!_featureFlags.isWebEnabled) { return Device[]; } /* ... 省略 ... */ } }次选方式globals模式直接读取全局 getter。features.dart L13 提供了从依赖注入上下文取出当前实现的入口FeatureFlags get featureFlags context.getFeatureFlags()!;例如flutter create在生成 iOS/macOS 插件工程时依据featureFlags.isSwiftPackageManagerEnabled决定是否追加plugin_swift_package_manager模板final ListString templates String[plugin, plugin_shared]; if ((isIos || isMacos) featureFlags.isSwiftPackageManagerEnabled) { templates.add(plugin_swift_package_manager); }在框架运行时消费开关runtimeId与 dart-define 通道工具侧与框架侧之间没有直接调用关系二者的桥梁是一条编译期注入的通道这也是文档Limitations一节原理的核心。工具在构建时把所有已启用且声明了runtimeId的开关拼接成一个 dart-define。在 flutter_command.dart 的_addFeatureFlagsToDartDefinesfinal String enabledFeatureFlags featureFlags.allFeatures .where((Feature feature) featureFlags.isEnabled(feature)) .where((Feature feature) feature.runtimeId ! null) .map((Feature feature) feature.runtimeId!) .join(,); if (enabledFeatureFlags.isNotEmpty) { dartDefines.add($kEnabledFeatureFlags$enabledFeatureFlags); }其中kEnabledFeatureFlags定义为字符串FLUTTER_ENABLED_FEATURE_FLAGSbuild_info.dart L1128。同时该函数会拦截用户通过--dart-define手动设置同名键的行为并直接报错提示改用flutter config——这条保护保证了框架与工具对开关状态的一致性。框架侧在 packages/flutter/lib/src/foundation/_features.dart 中读取该常量并暴露给业务代码/// The feature flags this app was built with. internal final SetString debugEnabledFeatureFlags String{ ...const String.fromEnvironment(FLUTTER_ENABLED_FEATURE_FLAGS).split(,), };框架内消费开关的示例文档原例enable-sensitive-content为实验特性import package:flutter/src/foundation/_features.dart; final class SensitiveContent extends StatelessWidget { SensitiveContent() { if (!debugEnabledFeatureFlags.contains(enable-sensitive-content)) { throw UnsupportedError(Sensitive content is an experimental feature and not yet available.); } } }仓库中的既有先例还包括_features.dart里的isWindowingEnabledruntimeId: windowing与isAccessibilityEvaluationsEnabledruntimeId: accessibility_evaluations两者均标注internal并声明可能随时做破坏性变更与文档框架侧运行时开关用法非常新、会持续演进的提示一致。文档还特别强调一条限制特性开关不是为 tree shaking 设计的。你无法根据开关值条件性地 import Dart 代码被开关关闭的代码不一定会被摇树移除。引擎与 embedder 为什么不能直接读开关Limitations一节给出了一个容易踩坑的架构事实Flutter 引擎和 embedder 无法在运行时直接查询 Flutter 的特性开关。原因是工具位于平台构建的上游可以把开关的值喂进构建流程烧录进平台配置由引擎/embedder 读取。文档以enable-hcpp为例工具把该值以-Penable-hcpp传给 GradleFlutter Gradle 插件再向合并后的 manifest 注入io.flutter.embedding.android.EnableHcpp元数据除非 manifest 已显式设置最终优先级为--[no-]enable-hcppAndroidManifest.xml 特性开关。这与仓库中 hcpp Feature 的注释 完全对应。文档同时给出该注入技术的有效边界只有当注入的配置不会与开发者自有配置冲突时才成立。例如EnableHcpp注入仅限 application 工程——若注入到 add-to-app 模块aar的 manifest会与宿主应用 manifest 合并而宿主中冲突的显式值会在 Android manifest merger 阶段直接构建失败而不是宿主优先。若你的 embedder 需要开关而又不存在上述管道文档推荐直接使用平台自有配置AndroidAndroidManifest.xmlmanifest xmlns:androidhttp://schemas.android.com/apk/res/android application ... meta-data android:nameio.flutter.embedding.android.EnableUnicornEmojis android:valuetrue / /application /manifestiOS / macOSInfo.plist... plist version1.0 dict keyFLTEnableUnicornEmojis/key true / /dict /plist文档在此处引用 Impeller 与 UI thread merging 作为既有先例并给出明确的偏好建议尽可能优先使用 Flutter 特性开关而非平台配置文件因为对 Flutter 应用开发者来说更容易。测试中的开关用法集成测试integration tests对于代表某个包、开关已启用的集成测试文档推荐在pubspec.yaml中用config:属性声明flutter: config: enable-unicorn-emojis: true你可能还会见到用flutter config全局启用/关闭开关的遗留写法新代码应优先前者。工具单元测试当被测代码显式接收FeatureFlags实例时直接构造测试替身。仓库中的TestFeatureFlagspackages/flutter_tools/test/src/fakes.dart L563实现FeatureFlags接口所有字段默认 falseisAndroidEnabled/isIOSEnabled默认 true按名传参即可final WindowsWorkflow windowsWorkflow WindowsWorkflow( platform: windows, featureFlags: TestFeatureFlags(isWindowsEnabled: true), ); /* ... 省略 ... */对于大型测试套件或走全局featureFlagsgetter 的代码则用testUsingContext的overrides替换上下文中的FeatureFlags实现testUsingContext(prints unicorns when enabled, () async { // 这里只是示例实际应写真实断言。 expect(featureFlags.isUnicornEmojisEnabled, true); }, overrides: Type, Generator{ FeatureFlags: () TestFeatureFlags(isUnicornEmojisEnabled: true), });框架单元测试框架侧开关是编译期常量集合测试中通过临时改写debugEnabledFeatureFlags并注册 teardown 恢复现场test(sensitive content should fail if the flag is disabled, () { final SetString originalFeatureFlags {...debugEnabledFeatureFlags}; addTearDown(() { debugEnabledFeatureFlags.clear(); debugEnabledFeatureFlags.addAll(originalFeatureFlags); }); debugEnabledFeatureFlags.remove(enable-sensitive-content); expect(() SensitiveContent(), throwsUnsupportedError); });移除一个开关当开关不再有用实验结束、或已默认开启并完整发布过一个以上 stable 版本绝大多数开关应当被移除以便把旧行为或缺乏该特性的代码路径从代码库中清理掉并减少开关之间互相冲突的可能。文档用脚注补充少数开关可能有较长甚至无限寿命但这很罕见。移除步骤与新增完全相反从allFeatures与FlutterFeatureFlagsIsEnabled、FeatureFlags接口中删掉 getter 和字段删除顶层Feature常量同步更新 google3 一侧的Google3Features并清理单元测试与集成测试中对该开关的引用。小结生命周期阶段关键操作源码锚点新增顶层Feature常量 接口 getter mixin 实现 allFeatures注册features.dart、flutter_features.dart可用化渠道设置available: trueFeatureChannelSetting默认开启enabledByDefault: true可逐渠道如 hcpp毕业Feature.fullyEnabled(...)如 flutterWebFeature取值解析渠道可用性 pubspec 全局 config 环境变量 渠道默认flutter_features_config.dart框架消费runtimeId→FLUTTER_ENABLED_FEATURE_FLAGSdart-define →debugEnabledFeatureFlagsflutter_command.dart L1460-L1479、_features.dart测试config:段 /TestFeatureFlags/ 改写debugEnabledFeatureFlagsfakes.dart L563移除反向执行新增步骤清理测试引用贡献文档 Feature-flags.md掌握这套机制后你在为 Flutter 工具或框架添加任何灰度特性时都能保证默认安全不可用即不可达、按渠道可控地放大、取值来源清晰可追溯、测试可独立翻转任意开关并且最终可以干净地毕业或移除。【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表