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.) and do any kind of work to make enabled() / tweak(Boolean) functions operate as needed. The code below automatically wires the tweak into the system, so the Tweaks class can pick it up.

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

2️⃣ 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.