| # 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). | |
| --- | |
| 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) | |
| - [Precedence](#precedence) | |
| - [Using a flag to drive behavior](#using-a-flag-to-drive-behavior) | |
| - [Tool](#tool) | |
| - [Framework](#framework) | |
| - [Tests](#tests) | |
| - [Limitations](#limitations) | |
| ## 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 change 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; | |
| } | |
| ``` | |
| 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 | |
| 1. Add a new entry in `FeatureFlags.allFeatures`: | |
| ```dart | |
| List<Feature> get allFeatures => const <Feature>[ | |
| // ... | |
| unicornEmojis, | |
| ]; | |
| ``` | |
| 1. Create a [G3Fix][] to update google3's [`Google3Features`][]: | |
| [G3Fix]: http://go/g3fix | |
| [`Google3Features`]: http://go/flutter-google3features | |
| 1. Add a new field to `Google3Features` : | |
| ```dart | |
| class Google3Features extends FeatureFlags { | |
| @override | |
| bool get isUnicornEmojisEnabled => true; | |
| } | |
| ``` | |
| 2. Add a new entry to `Google3Features.allFeatures`: | |
| ```dart | |
| List<Feature> get allFeatures => const <Feature>[ | |
| // ... | |
| unicornEmojis, | |
| ]; | |
| ``` | |
| ### 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. | |
| ### Precedence | |
| Users have several options to configure flags. Assuming the following feature: | |
| ```dart | |
| const Feature unicornEmojis = Feature( | |
| name: 'add unicorn emojis in lots of fun places', | |
| configSetting: 'enable-unicorn-emojis', | |
| environmentOverride: 'FLUTTER_ENABLE_UNICORN_EMOJIS', | |
| ); | |
| ``` | |
| Flutter uses the following precedence order: | |
| 1. The app's `pubspec.yaml` file: | |
| ```yaml | |
| flutter: | |
| config: | |
| enable-unicorn-emojis: true | |
| ``` | |
| 2. The tool's global configuration: | |
| ```sh | |
| flutter config --enable-unicorn-emojis | |
| ``` | |
| 3. Environment variables: | |
| ```sh | |
| FLUTTER_ENABLE_UNICORN_EMOJIS=true flutter some-command | |
| ``` | |
| If none of these are set, Flutter falls back to the feature's | |
| default value for the current release channel. | |
| ## 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<List<Device>> pollingGetDevices({Duration? timeout}) async { | |
| if (!_featureFlags.isWebEnabled) { | |
| return <Device>[]; | |
| } | |
| /* ... 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<int> _generateMethodChannelPlugin() async { | |
| /* ... omitted for brevity ... */ | |
| final List<String> templates = <String>['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. | |
| Feature flags are not designed to help tree shaking. For example, you | |
| cannot conditionally import Dart code depending on the enabled feature flags. | |
| Tree shaking might not remove code that is feature flagged off. | |
| ### 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: <Type, Generator>{ | |
| 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<String> 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. | |
| # Limitations | |
| The Flutter engine and embedders cannot query Flutter's feature flags directly at runtime. | |
| However, the Flutter tool runs upstream of the platform builds, so it can query a | |
| feature flag and pipe the value into the build, where it is baked into the | |
| platform-specific configuration that the engine or embedder reads. For example, the | |
| `enable-hcpp` flag is passed to Gradle as `-Penable-hcpp`, and the Flutter Gradle | |
| Plugin injects the corresponding `io.flutter.embedding.android.EnableHcpp` manifest | |
| metadata into the merged manifest, unless the merged manifest already sets it. The | |
| resulting precedence is `--[no-]enable-hcpp` > `AndroidManifest.xml` > the feature | |
| flag. | |
| Note that this technique only works where the injected configuration cannot conflict | |
| with configuration the developer owns. For example, the `EnableHcpp` injection is | |
| limited to application projects: injecting into an add-to-app module (aar) manifest | |
| would merge into the host app's manifest, where a conflicting explicit value in the | |
| host manifest fails the host build in the Android manifest merger rather than taking | |
| priority. | |
| If an embedder needs feature flags and no such piping exists, you can instead use the project's platform-specific configuration. | |
| On Android, use `AndroidManifest.xml`: | |
| ```xml | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |
| <application ...> | |
| <meta-data | |
| android:name="io.flutter.embedding.android.EnableUnicornEmojis" | |
| android:value="true" /> | |
| </application> | |
| </manifest> | |
| ``` | |
| On iOS and macOS, use `Info.plist`: | |
| ```xml | |
| ... | |
| <plist version="1.0"> | |
| <dict> | |
| <key>FLTEnableUnicornEmojis</key> | |
| <true /> | |
| </dict> | |
| </plist> | |
| ``` | |
| See Impeller and UI thread merging for prior art. | |
| > [!IMPORTANT] | |
| > If possible, prefer to use Flutter feature flags instead of platform-specific configuration files. | |
| > Flutter feature flags are easier for Flutter app developers. |