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
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.
kotlin1@Inject2@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 {1112 private var cachedState = true1314 override suspend fun enabled(): Boolean {15 val appInfo = appConfigManager.appConfig.first().info16 return appInfo.debug && cachedState17 }1819 override suspend fun tweak(enabled: Boolean) {20 cachedState = enabled21 }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.
kotlin1@Inject2@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>() {1011 private val profileState = mutableState(false) {12 tweaks.enabled(AppFeature.PROFILE)13 }1415 @Composable16 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.