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

Add PROFILE to the AppFeature enum, then follow these two steps to implement and use the new 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@ContributesIntoMap(AppScope::class)
2@FeatureKey(AppFeature.PROFILE)
3// You can add `@SingleIn(AppScope::class)` to make it behave as a singleton,
4// so it can store your runtime variables in memory, like `cachedState` below.
5// In other cases `@SingleIn(AppScope::class)` is redundant.
6@SingleIn(AppScope::class)
7class ProfileFeatureTweak(
8 private val appConfigManager: AppConfigManager,
9) : FeatureTweak {
10
11 private var cachedState = true
12
13 override suspend fun enabled(): Boolean {
14 val appInfo = appConfigManager.appConfig.first().appInfo
15 return appInfo.debug && cachedState
16 }
17
18 override suspend fun tweak(enabled: Boolean) {
19 cachedState = enabled
20 }
21}

2️⃣ Use Tweaks to read or flip the state

Inject Tweaks wherever the UI needs to react to the feature gate. ViewModels 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@ViewModelKey
2@ContributesIntoMap(AppScope::class, binding<ViewModel>())
3class ProfileEntryViewModel(
4 private val tweaks: Tweaks,
5) : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {
6
7 private val eventSink = createEventSink(::handleEvent)
8 private val profileEnabledState = mutableState(false) {
9 tweaks.enabled(AppFeature.PROFILE)
10 }
11
12 @Composable
13 override fun state(): ProfileUiState {
14 val enabled by profileEnabledState.collectAsStateWithLifecycle()
15 return ProfileUiState(
16 enabled = enabled,
17 eventSink = eventSink,
18 )
19 }
20
21 private fun handleEvent(event: ProfileUiEvent) {
22 when (event) {
23 ProfileUiEvent.ToggleProfile -> handleToggleProfile()
24 }
25 }
26
27 private fun handleToggleProfile() {
28 launch {
29 val enabled = profileEnabledState.value
30 tweaks.tweak(AppFeature.PROFILE, !enabled)
31 // If the current UI must reflect the updated tweak value,
32 // recreate the state after applying the change.
33 profileEnabledState.recreate()
34 }
35 }
36}

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.