mirror of
https://github.com/flutter/flutter.git
synced 2026-01-14 17:35:51 +08:00
441 lines
12 KiB
Markdown
441 lines
12 KiB
Markdown
# 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 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;
|
|
}
|
|
```
|
|
|
|
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 precendence 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 use Flutter's feature flags directly.
|
|
|
|
If an embedder needs feature flags, 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.
|