Skip to content

Commit fb1baad

Browse files
authored
docs(feature-flag) new document describing feature flags (#10966)
2 parents eae8250 + 92c7b8e commit fb1baad

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

core/featureflag/src/commonMain/kotlin/net/thunderbird/core/featureflag/FeatureFlagKey.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ package net.thunderbird.core.featureflag
33
@JvmInline
44
value class FeatureFlagKey(val key: String) {
55
companion object Keys {
6+
/**
7+
* DO NOT ADD NEW FEATURE FLAGS HERE.
8+
*
9+
* New feature flags should be added to an object in the `:api` module of the feature
10+
* they belong to, to avoid tight coupling.
11+
* See `docs/architecture/feature-flags.md` for more details.
12+
*/
613
val DisplayInAppNotifications = "display_in_app_notifications".toFeatureFlagKey()
714
val UseNotificationSenderForSystemNotifications =
815
"use_notification_sender_for_system_notifications".toFeatureFlagKey()

docs/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ generator, in this case, **mdbook**. It defines the structure and navigation of
2727
- [Design System](architecture/design-system.md)
2828
- [User Flows](architecture/user-flows.md)
2929
- [Settings](architecture/settings.md)
30+
- [Feature Flags](architecture/feature-flags.md)
3031
- [Legacy Module Integration](architecture/legacy-module-integration.md)
3132
- [Architecture Decision Records](architecture/adr/README.md)
3233
- [Accepted]()
118 KB
Loading

docs/architecture/feature-flags.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Feature Flags
2+
3+
A feature flag can be nothing more than a simple boolean that can enable or disable a feature that we're working on. While some companies may use their feature flags for A/B testing, with timestamps controlling releases, and any other features they may want more control over. We primarily use them to turn off new features before they're ready to be released to the wider userbase. This is in part because we work on the app using our own forked repositories, and rarely create feature branches to merge ongoing projects into. This isn't a problem because we use local feature flags on debugging builds only, and allows easier testing of new features we're working on.
4+
5+
In this guide, you'll learn where our feature flags live, how to add your own, and how to make use of them. It'll come in handy if you want to work on a larger project than one single PR could create.
6+
7+
#### When Should You Add a Feature Flag?
8+
9+
We use feature flags to close off parts of the app that aren’t for public consumption yet. We put our feature flags in our code, not using remote services, and if you build a debug version of the Thunderbird or K9 app, you can choose which feature flags to test for yourself. This comes at some risk. These are in-development features, features that were too large to put into the app in one commit, or could potentially break the app and need further testing before we can enable them for our production users. However, our contributors know what they’re doing, and understand that these features are incomplete.
10+
11+
If you’re making something that’s too large for a single pull request, or believe it needs to be tested alongside existing code, a feature flag is the perfect way to get started.
12+
13+
## Important Feature Flag Classes
14+
15+
First, a quick overview on the classes you'll be working with to create your feature flag.
16+
17+
- `FeatureFlag`
18+
- Package: `net.thunderbird.core.featureflag`
19+
- Data class, takes a `FeatureFlagKey` and an `enabled` boolean, defaults to false
20+
- Example: `FeatureFlag(MessageReaderFeatureFlags.UseComposeForMessageReader)`
21+
- This would be set automatically to false
22+
- `FeatureFlagKey`
23+
- Passed in to the `FeatureFlag` class as the first parameter
24+
- Defines the string that will serve as the key for a feature flag
25+
- Constructor takes in a single string to use as the key
26+
- The preferred pattern would be to place keys in the `:api` module for your feature, not in this class (see [How to Add a Feature Flag](#how-to-add-a-feature-flag)).
27+
- Has a utility function for Strings, `String.toFeatureFlagKey` which is just `FeatureFlagKey(this)`
28+
- Can pass in a pre-defined string or a string literal along with the `toFeatureFlag()` extension function to generate aa `FeatureFlagKey`
29+
- Example: `val myFeatureFlagKey = "my_feature_flag_key".toFeatureFlagKey()`
30+
- `DefaultFeatureFlagOverrides`
31+
- Implements the `FeatureFlagOverrides` interface
32+
- Creates a catalog of all available feature flags for a particular build/application/module
33+
- Mostly used for its `getCatalog()` function, which returns a `Flow<List<FeatureFlag>>`
34+
- Also has the ability to get/set individual feature flags by `FeatureFlagKey`, as well as clear one feature flag or all of them in the collection
35+
- `FeatureFlagFactory`
36+
- Interface that has to be implemented in each application/module
37+
- K9: `single<FeatureFlagFactory> { K9FeatureFlagFactory() }`
38+
- `app.k9mail.featureflag` package
39+
- `appModule` defines it in the `K9KoinModule` file
40+
- Thunderbird: `single<FeatureFlagFactory> { TbFeatureFlagFactory() }`
41+
- Located in `net.thunderbird.android.featureflag` package
42+
- in the `appModule` definition, `ThunderbirdKoinModule.kt`
43+
- The classes implementing it will be where your feature flags "live," where you define them and where they're set.
44+
- `TbFeatureFlagFactory`
45+
- Implementation of `FeatureFlagFactory` for the Thunderbird App
46+
- Contains the feature flags for the Thunderbird app
47+
- Package: `net.thunderbird.android.featureflag`
48+
- Different versions in different directories to define the build. Each build will use the `TbFeatureFlagFactory` from its own directory. See the "[How to Add a Feature Flag](#how-to-add-a-feature-flag)" section below to find all of the factory locations you'll have to change to create a new flag
49+
- Creates a flow for `getCatalog()` so updates to the flags can trigger downstream listeners/collectors
50+
- You can set up feature flags in specific classes, but they still have to be named individually here, for example, these are feature flags related to MessageList:
51+
- `FeatureFlag(MessageListFeatureFlags.UseComposeForMessageListItems, enabled = false),`
52+
- `FeatureFlag(MessageListFeatureFlags.EnableMessageListNewState, enabled = false),`
53+
- These are both defined in the `MessageListFeatureFlags` object in the `net.thunderbird.feature.mail.message.list` package
54+
- `K9FeatureFlagFactory`
55+
- Package: `app.k9mail.featureflag`
56+
- This is the same as the `TbFeatureFlagFactory` class, implementing the `FeatureFlagFactory` interface for the K9 app
57+
58+
## How to Add a Feature Flag
59+
60+
Now that you're ready to add your own feature flag to the app, you need to answer a few questions. Where do you put your feature flag definition, where do you generate the feature flag itself, and how do you provide it to the apps. Here's how to do those steps.
61+
62+
#### Decide Where Your Feature Flag Belongs
63+
64+
Place your feature flag in the feature you’re working on. For example, a MessageList feature flag object, `MessageListFeatureFlags`, lives in the `net.thunderbird.feature.mail.message.list` package. It’s specifically in `feature/mail/message/list/api/src/main/kotlin/net/thunderbird/feature/mail/message/list/MessageListFeatureFlags.kt`. Note that it’s placed in the api directory.
65+
66+
#### Make the Feature Flag
67+
68+
If there’s already an object, you can simply add a new `FeatureFlagKey(val key: String)` object, like so: `val UseComposeForMessageReader = FeatureFlagKey("use_compose_for_message_reader")`. Otherwise, you’ll want to make a simple object to hold your feature flag (or flags). That might look something like this:
69+
70+
```kotlin
71+
object MessageReaderFeatureFlags {
72+
val UseComposeForMessageReader = FeatureFlagKey("use_compose_for_message_reader")
73+
}
74+
```
75+
76+
#### Add Your Feature Flag to the Providers
77+
78+
Your feature flag will have to be in every app the feature will be available as well as all the build types. That includes release and debug, even if you won’t be adding it to a release build for some time.
79+
Remember, we also will want to set the default value to turn the feature off.
80+
81+
You’ll be adding your `FeatureFlag` to the `FeatureFlagFactory` implementation for your build. This will include a line like this in the `getCatalog()` function:
82+
83+
```kotlin
84+
override fun getCatalog(): Flow<List<FeatureFlag>> = flow {
85+
emit(
86+
listOf(
87+
...
88+
FeatureFlag(MessageReaderFeatureFlags.UseComposeForMessageReader, enabled = false),
89+
),
90+
)
91+
}
92+
```
93+
94+
Here are the locations you might have to add it to currently. However, as time goes on, we may have other build flavors and apps to add it to, so be sure to check.
95+
96+
**For Thunderbird:**
97+
98+
- `app-thunderbird/src/release/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt`
99+
- `app-thunderbird/src/beta/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt`
100+
- `app-thunderbird/src/daily/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt`
101+
- `app-thunderbird/src/debug/kotlin/net/thunderbird/android/featureflag/TbFeatureFlagFactory.kt`
102+
103+
**For K9 Mail:**
104+
105+
- `app-k9mail/src/release/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt`
106+
- `app-k9mail/src/debug/kotlin/app/k9mail/featureflag/K9FeatureFlagFactory.kt`
107+
108+
Creating a feature flag to start your work is enough to create a pull request. Doing work in small parts like this is what feature flags enable, and it’s perfectly acceptable to submit a pull request for feature flags alone, as long as you’re linking to the full project in the description so a reviewer knows what you intend to use the flag for.
109+
110+
#### Ensure Your Flag Is Part of the Build
111+
112+
Ensure to include `projects.core.featureflag` in the feature's gradle file. For example, for a flag related to the message reader, I'd ensure we have the common dependency mentioned in the `build.gradle.kts` file located in `feature/mail/message/reader/api/build.gradle.kts` like so:
113+
114+
```kotlin
115+
kotlin {
116+
...
117+
sourceSets {
118+
commonMain.dependencies {
119+
...
120+
implementation(projects.core.featureflag)
121+
}
122+
}
123+
}
124+
```
125+
126+
#### Accessing Your Feature Flag
127+
128+
The feature flags for each build are provided by Koin. You’ll get an instance of the `FeatureFlagProvider` for your build with `val featureFlagProvider = get<FeatureFlagProvider>()`. From there, you can access your feature flag with the key like so:
129+
130+
```kotlin
131+
if (featureFlagProvider.provide(MessageReaderFeatureFlags.UseNewMessageReaderCssStyles).isEnabled()) {
132+
// Do the thing
133+
}
134+
```
135+
136+
You can certainly store the value separately as well
137+
138+
```kotlin
139+
val composeForMessageReader = featureFlagProvider.provide(UseComposeForMessageReader)
140+
...
141+
if (composeForMessageReader.isEnabled()) {
142+
// Do the thing
143+
}
144+
```
145+
146+
## Ensuring Contributors Know About Your Feature Flag
147+
148+
After you've added a feature flag, you should make a pull request just for the flag itself. If you're working on an incremental project, it makes sense to break it up as much as possible, and can ensure you have a working feature flag in the app quickly, so you can start to do the work you'll put behind it.
149+
150+
After you've added the feature flag into the codebase and added your own code that will sit behind it, you should also ensure that anyone looking at the subsequent pull requests behind that feature flag know to use it to test the feature. It can also make projects associated with the same feature flag easier to search for and review in context later.
151+
152+
When you create a PR, use the `feature-flag` label in GitHub. This is found on the right sidebar in your pull request. Also, mention the feature flag directly in the text of your pull request. This can be as simple as a line like "feature flag: `your_feature_flag`"
153+
154+
## Seeing Your Feature Flag
155+
156+
![In-app debug screen with feature flags and switches](assets/secretDebugScreenFeatureFlags.jpg)
157+
158+
You're probably thinking you have to add the feature flag to some list now, right? Wrong! When you do a debug build, you'll be able to access the new feature flag right away. You can find the feature flag you've just made in the "Secret Debug Settings Screen." This is displayed by the `DebugFeatureFlagSection` composable function. You'll be able to enable your feature flag and test your new feature right from here. To access the "Secret Debug Settings Screen," you can either use the three dot button on the message list screen and select "DEBUG: Feature Flags," or you can go into the side menu, down to Settings, General Settings, Debugging, and tap "Open Secret debug screen."
159+
160+
Enjoy testing your new feature!
161+

0 commit comments

Comments
 (0)