From ea010f3e44d815ccce8859bd4fa6b0b4677b254c Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 18 Jun 2025 12:02:33 -0700 Subject: [PATCH] Add an initial "Using feature flags" doc for the team. (#170767) Closes https://github.com/flutter/flutter/issues/170456. There are still some TODOs I'd like to address, but in follow-up PRs: - Update the `# Framework` sub-sections after #168437 merges - Add known limitations (i.e. embedder/engine) - Add best practices (using a feature flag versus just using a parameter, named constructor, etc) --- docs/contributing/Feature-flags.md | 333 +++++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 docs/contributing/Feature-flags.md diff --git a/docs/contributing/Feature-flags.md b/docs/contributing/Feature-flags.md new file mode 100644 index 00000000000..9043429ece1 --- /dev/null +++ b/docs/contributing/Feature-flags.md @@ -0,0 +1,333 @@ +# Using feature flags + +[flutter.dev/to/feature-flags](https://flutter.dev/to/feature-flags) + +The Flutter tool (`flutter`) supports the concept of _feature flags_, or boolean +flags that can inform, change, allow, or deny access to behavior, either in the +tool itself, or in the framework (`package:flutter`, and related). + +> [!WARNING] +> +> This document is based on the unmerged PR [#168437](https://github.com/flutter/flutter/pull/168437). + +--- + +Table of Contents + +- [Overview](#overview) + - [Why feature flags](#why-feature-flags) +- [Adding a flag](#adding-a-flag) + - [Allowing flags to be enabled](#allowing-flags-to-be-enabled) + - [Enabling a flag by default](#enabling-a-flag-by-default) + - [Removing a flag](#removing-a-flag) +- [Using a flag to drive behavior](#using-a-flag-to-drive-behavior) + - [Tool](#tool) + - [Framework](#framework) + - [Tests](#tests) + +## Overview + +For example, [enabling the use of Swift Package Manager][enable-spm]: + +```sh +flutter config --enable-swift-package-manager +``` + +Feature flags can be configured globally (for an entire _machine_), locally +(for a particular _app_), per-test, and be automatically enabled for different +release channels (`master`, versus `beta`, versus `stable`), giving multiple +consistent options for developing. + +_See also, [Flutter pubspec options > Fields > Config][pubspec-config]._ + +[enable-spm]: https://docs.flutter.dev/packages-and-plugins/swift-package-manager/for-app-developers +[pubspec-config]: https://docs.flutter.dev/tools/pubspec#config + +### Why feature flags + +Feature flags allow conditionally, consistently, and conveniently changing +behavior. + +For example: + +- **Gradual rollouts** to introduce new features to a small subset of users. + +- **A/B Testing** to easily test or compare different implementations. + +- **Kill Switches** to quickly disable problematic features without large code + changes. + +- **Allow experimental access** to features not ready for broad or unguarded + use. + + We do not consider it a breaking change to modify or remove experimental flags + across releases, or to make changes guarded by experimental flags. APIs that + are guarded by flags are subject to chage at any time. + +## Adding a flag + +Flags are managed in [`packages/flutter_tools/lib/src/features.dart`][flag-path]. + +[flag-path]: ../../packages/flutter_tools/lib/src/features.dart + +The following steps are required: + +1. Add a new top-level `const Feature`: + + ```dart + const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + ); + ``` + + Additional parameters are required to make the flag configurable outside of + a [unit test](#tests). + + To allow `flutter config`, or in `pubspec.yaml`'s `config: ...` section, + include `configSetting`: + + ```dart + const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + configSetting: 'enable-unicorn-emojis', + ); + ``` + + To allow usage of the flag in the Flutter framework, include `runtimeId`: + + ```dart + const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + runtimeId: 'enable-unicorn-emojis', + ); + ``` + + To allow an environment variable, include `environmentOverride`: + + ```dart + const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + environmentOverride: 'FLUTTER_UNICORN_EMOJIS', + ); + ``` + +1. Add a new field to `abstract class FeatureFlags`: + + ```dart + abstract class FeatureFlags { + /// Whether to add unicorm emojis in lots of fun places. + bool get isUnicornEmojisEnabled => false; + } + ``` + +1. Implement the same getter in [`FlutterFeatureFlagsIsEnabled`][]: + + ```dart + mixin FlutterFeatureFlagsIsEnabled implements FeatureFlags { + @override + bool get isUnicornEmojisEnabled => isEnabled(unicornEmojis); + } + ``` + + [`FlutterFeatureFlagsIsEnabled`]: ../../packages/flutter_tools/lib/src/flutter_features.dart + +### Allowing flags to be enabled + +By default, after [adding a flag](#adding-a-flag), the flag is considered +_disabled_, and _cannot_ be enabled outside of our own [unit tests](#tests). +This allows iterating locally with the code without having to support users or +field issues related to the flag. + +After some time, you may want to allow the flag to be enabled. + +Using the options `master`, `beta` or `stable`, you can make the flag +configurable in those channels. For example, to make the flag available to be +enabled (but still off by default) on the `master` channel: + +```dart +const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + configSetting: 'enable-unicorn-emojis', + master: FeatureChannelSetting(available: true), +); +``` + +Or to make it available on all channels: + +```dart +const Feature unicornEmojis = Feature( + name: 'add unicorn emojis in lots of fun places', + configSetting: 'enable-unicorn-emojis', + master: FeatureChannelSetting(available: true), + beta: FeatureChannelSetting(available: true), + stable: FeatureChannelSetting(available: true), +); +``` + +### Enabling a flag by default + +Once a flag is ready to be enabled by default, once again it can be configured +on a per-channel basis. + +For example, enabled on `master` by default, but disabled by default elsewhere: + +```dart +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), +); +``` + +Once the flag is ready to be enabled in every environment: + +```dart +const Feature unicornEmojis = Feature.fullyEnabled( + name: 'add unicorn emojis in lots of fun places', + configSetting: 'enable-unicorn-emojis', +); +``` + +### Removing a flag + +After a flag is no longer useful (perhaps the experiment has concluded, the +flag has been enabled successfully for 1+ stable releases), _most_[^1] flags +should be removed so that the older behavior (or lack of a feature) can be +refactored and removed from the codebase, and there is less of a possibility of +conflicting flags. + +To remove a flag, follow the opposite steps of +[adding a flag](#adding-a-flag). + +You may need to remove references to the (disabled) flag from unit or +integration tests as well. + +[^1]: Some flags might have a longer or indefinite lifespan, but this is rare. + +## Using a flag to drive behavior + +Once you have a flag, you can use it to conditionally enable something or +provide a different execution branch. + +### Tool + +In the `flutter` tool, feature flags. flags can be accessed either by adding +(and providing) an explicit `FeatureFlags` parameter (**recommended**): + +```dart +class WebDevices extends PollingDeviceDiscovery { + // While it could be injected from the global scope (see below), this larger + // feature (and tests of it) are made more explicit by directly taking a + // reference to a `FeatureFlags` instance. + WebDevices({required FeatureFlags featureFlags}) : _featureFlags = featureFlags; + + final FeatureFlags _featureFlags; + + @override + Future> pollingGetDevices({Duration? timeout}) async { + if (!_featureFlags.isWebEnabled) { + return []; + } + /* ... omitted for brevity ... */ + } +} +``` + +Or by injecting the currently set flags using the `globals` pattern: + +```dart +// Relative path depends on location in the tool. +import '../src/features.dart'; + +class CreateCommand extends FlutterCommand with CreateBase { + Future _generateMethodChannelPlugin() async { + /* ... omitted for brevity ... */ + final List templates = ['plugin', 'plugin_shared']; + if ((isIos || isMacos) && featureFlags.isSwiftPackageManagerEnabled) { + templates.add('plugin_swift_package_manager'); + } + /* ... omitted for brevity ... */ + } +} +``` + +### Framework + +In the framework, feature flags can be accessed by importing +`src/foundation/_features.dart`: + +```dart +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.'); + } + } +} +``` + +Note that feature flag usage in the framework runtime is very new, and is likely +to evolve over time. + +### Tests + +#### Integration tests + +For integration tests representing _packages_ where a flag is enabled, prefer +using the [`config:`][pubspec-config] property in `pubspec.yaml`: + +```yaml +flutter: + config: + enable-unicorn-emojis: true +``` + +You may see legacy cases where the flag is enabled or disabled globally using +`flutter config`. + +#### Tool unit tests + +For unit tests where the code directly takes a `FeatureFlags` instance: + +```dart +final WindowsWorkflow windowsWorkflow = WindowsWorkflow( + platform: windows, + featureFlags: TestFeatureFlags(isWindowsEnabled: true), +); +/* ... omitted for brevity ... */ +``` + +Or, for larger test suites, or code that uses the global `featureFlags` getter: + +```dart +testUsingContext('prints unicorns when enabled', () async { + // You'd write a real test, this is just an example. + expect(featureFlags.isUnicornEmojisEnabled, true); +}, overrides: { + FeatureFlags: () => TestFeatureFlags(isUnicornEmojisEnabled: true), +}); +``` + +#### Framework unit tests + +Feature flags can be enabled by importing `src/foundation/_features.dart`: + +```dart +test('sensitive content should fail if the flag is disabled', () { + final Set originalFeatureFlags = {...debugEnabledFeatureFlags}; + addTearDown(() { + debugEnabledFeatureFlags.clear(); + debugEnabledFeatureFlags.addAll(originalFeatureFlags); + }); + + debugEnabledFeatureFlags.remove('enable-sensitive-content'); + expect(() => SensitiveContent(), throwsUnsupportedError); +}); +``` + +Note that feature flag usage in the framework runtime is very new, and is likely +to evolve over time.