mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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)
This commit is contained in:
parent
5ecde8a2c4
commit
ea010f3e44
333
docs/contributing/Feature-flags.md
Normal file
333
docs/contributing/Feature-flags.md
Normal file
@ -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<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.
|
||||
|
||||
### 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.
|
||||
Loading…
x
Reference in New Issue
Block a user