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:
AppFeatureenumFeatureTweakinterfaceTweaksfacade
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.
kotlin1@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 {1011 private var cachedState = true1213 override suspend fun enabled(): Boolean {14 val appInfo = appConfigManager.appConfig.first().appInfo15 return appInfo.debug && cachedState16 }1718 override suspend fun tweak(enabled: Boolean) {19 cachedState = enabled20 }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.
kotlin1@ViewModelKey2@ContributesIntoMap(AppScope::class, binding<ViewModel>())3class ProfileEntryViewModel(4 private val tweaks: Tweaks,5) : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {67 private val eventSink = createEventSink(::handleEvent)8 private val profileEnabledState = mutableState(false) {9 tweaks.enabled(AppFeature.PROFILE)10 }1112 @Composable13 override fun state(): ProfileUiState {14 val enabled by profileEnabledState.collectAsStateWithLifecycle()15 return ProfileUiState(16 enabled = enabled,17 eventSink = eventSink,18 )19 }2021 private fun handleEvent(event: ProfileUiEvent) {22 when (event) {23 ProfileUiEvent.ToggleProfile -> handleToggleProfile()24 }25 }2627 private fun handleToggleProfile() {28 launch {29 val enabled = profileEnabledState.value30 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.