Feature Gates Usage Guide

Feature gates (also known as feature toggles or feature tweaks) allow you to turn pieces of functionality on or off at runtime without shipping a new build. This guide explains what they are, when to use them, and how to implement a new tweak inside the Baselines architecture.


What Is a Feature Gate?

A feature gate is a small abstraction that tells the rest of the app whether a feature is currently available. Under the hood it can talk to remote config, preferences, or any other store, but the public API always looks the same: check if the feature is enabled, optionally flip its value, and react in the UI based on the answer. In Baselines this contract lives in toolkit/feature-tweak module and is modeled by:

  • AppFeature enum
  • FeatureTweak interface
  • Tweaks facade

When Are Feature Tweaks Needed?

Use a tweak whenever you want control over exposing a feature without republishing the app. Typical cases include:

  • Gradually rolling out a capability to internal testers before going GA
  • Keeping experimental UI behind a guard so it can be disabled quickly
  • Building tooling (like the in-app Playground) where product or QA can toggle behaviors while exercising the app

If something must be safe to disable instantly—especially during early development—wrap it in a feature tweak.

Step-by-Step: Implement a New Tweak

Follow these three steps whenever you introduce a new gated feature. The snippets below assume we are adding a Profile feature flag.

1️⃣ Extend FeatureTweak

Create a class that implements FeatureTweak for your feature. The implementation can inject any dependencies it needs (config, storage, etc.) to make enabled() and tweak(Boolean) functions work.

kotlin
1@Inject
2// You can add `@SingleIn(AppScope::class)` to make it behave as a singleton,
3// so it can store your runtime variables in memory, like `cachedStated` below.
4// In other cases `@SingleIn(AppScope::class)` is redundant.
5@SingleIn(AppScope::class)
6class ProfileFeatureTweak(
7 private val appConfigManager: AppConfigManager,
8) : FeatureTweak {
9
10 private var cachedState = true
11
12 override suspend fun enabled(): Boolean {
13 val appInfo = appConfigManager.appConfig.first().info
14 return appInfo.debug && cachedState
15 }
16
17 override suspend fun tweak(enabled: Boolean) {
18 cachedState = enabled
19 }
20}

2️⃣ Provide the tweak instance into the map

Tweak implementations are exposed through DI as key/value pairs where the key is an AppFeature and the value is the FeatureTweak. Contribute your pair so the Tweaks class can pick it up.

kotlin
1@ContributesTo(AppScope::class)
2interface ProfileTweaksProvider {
3
4 @Provides
5 @IntoMap
6 fun provideProfileTweak(
7 impl: ProfileFeatureTweak,
8 ): Pair<AppFeature, FeatureTweak> = AppFeature.PROFILE to impl
9}

Add the new constant to AppFeature if it does not yet exist.

3️⃣ Use Tweaks to read or flip the state

Inject Tweaks wherever the UI needs to react to the feature gate. View models typically read the value inside a mutableState block and invoke tweak() when the user flips a toggle. HomeViewModel and the Playground FeatureTweakViewModel provide concrete examples.

kotlin
1@Inject
2@ContributesIntoMap(
3 scope = AppScope::class,
4 binding = binding<ViewModel>()
5)
6@ViewModelKey(ProfileViewModel::class)
7class ProfileEntryViewModel(
8 private val tweaks: Tweaks,
9) : BaselineViewModel<ProfileUiEvent, ProfileUiState>() {
10
11 private val profileState = mutableState(false) {
12 tweaks.enabled(AppFeature.PROFILE)
13 }
14
15 @Composable
16 override fun state(): ProfileUiState {
17 val enabled by profileState.collectAsStateWithLifecycle()
18 return ProfileUiState(enabled) { event ->
19 when (event) {
20 ProfileUiEvent.ToggleProfile -> launch {
21 tweaks.tweak(AppFeature.PROFILE, !enabled)
22 // If your current UI needs to react to the tweak change,
23 // simply recreate the state to pass the new state in.
24 profileState.recreate()
25 }
26 }
27 }
28 }
29}

With these pieces in place, the new feature gate becomes available throughout the app and can be controlled through the Playground tweaks UI or any other custom surface.