Feature flags decouple deployment from release. You ship code that's turned off, then toggle it on — for specific users, percentages of traffic, or in response to incidents — without an app update.
Why Feature Flags Matter
| Scenario | Without flags | With flags |
|---|---|---|
| Risky new feature | Big bang launch, hard to roll back | Gradual rollout; 1% → 10% → 100% |
| Incident response | Hotfix + emergency release | Toggle off via remote config in seconds |
| A/B testing | Manual variants, hard to measure | Server-controlled; automatic analytics |
| Per-tenant features | Multiple builds | One build; flags per tenant |
Firebase Remote Config
Firebase Remote Config is the most common Android flag solution:
// 1. Define defaults (values used before remote fetch)
val defaults = mapOf(
"new_feed_algorithm" to false,
"max_article_length" to 5000L,
"chat_enabled" to false
)
// 2. Initialize in Application.onCreate
val remoteConfig = Firebase.remoteConfig.apply {
setDefaultsAsync(defaults)
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
})
}
// 3. Fetch and activate
class FeatureFlagManager(private val remoteConfig: FirebaseRemoteConfig) {
suspend fun fetchAndActivate() {
remoteConfig.fetchAndActivate().await()
}
val isNewFeedEnabled: Boolean
get() = remoteConfig.getBoolean("new_feed_algorithm")
val maxArticleLength: Long
get() = remoteConfig.getLong("max_article_length")
val isChatEnabled: Boolean
get() = remoteConfig.getBoolean("chat_enabled")
}
Feature Flag Abstraction
Don't scatter remoteConfig.getBoolean(...) calls throughout your code. Create a typed abstraction:
interface FeatureFlags {
val isNewFeedEnabled: Boolean
val isChatEnabled: Boolean
val maxArticleLength: Int
}
class FirebaseFeatureFlags(
private val remoteConfig: FirebaseRemoteConfig
) : FeatureFlags {
override val isNewFeedEnabled get() = remoteConfig.getBoolean("new_feed_algorithm")
override val isChatEnabled get() = remoteConfig.getBoolean("chat_enabled")
override val maxArticleLength get() = remoteConfig.getLong("max_article_length").toInt()
}
// In tests — control all flags:
class TestFeatureFlags(
override val isNewFeedEnabled: Boolean = false,
override val isChatEnabled: Boolean = false,
override val maxArticleLength: Int = 5000
) : FeatureFlags
Using Flags in Code
class FeedViewModel(
private val repository: FeedRepository,
private val flags: FeatureFlags
) : ViewModel() {
fun loadFeed() = viewModelScope.launch {
val articles = if (flags.isNewFeedEnabled) {
repository.getPersonalizedFeed()
} else {
repository.getChronologicalFeed()
}
_state.value = UiState.Success(articles)
}
}
// In Compose:
@Composable
fun FeedScreen(flags: FeatureFlags = get()) {
if (flags.isChatEnabled) {
ChatFab(modifier = Modifier.align(Alignment.BottomEnd))
}
}
Gradual Rollout Strategy
// Firebase Remote Config supports conditions:
// - User property: "country = US"
// - App version: "version >= 5.0"
// - Random percentile: "Random 10%" (for 10% rollout)
// In Firebase Console → Remote Config → Add condition:
// Name: "10% early adopters"
// Condition: "User in Random Percentile" 0–10%
// Value for this condition: new_feed_algorithm = true
Kill Switch
The most important flag is a kill switch for a production incident:
class ArticleDetailViewModel(
private val flags: FeatureFlags,
private val repository: ArticleRepository
) : ViewModel() {
fun loadComments(articleId: String) {
if (!flags.isCommentsEnabled) {
// Comment system is having issues — show empty state
_commentsState.value = CommentsState.Disabled
return
}
// ... load comments
}
}
When an incident fires: toggle comments_enabled = false in Firebase console. Comments disappear for all users within the next fetch interval (~1 hour by default, or immediately with a force fetch).
Testing Feature Flags
@Test
fun `shows personalized feed when new algorithm enabled`() {
val flags = TestFeatureFlags(isNewFeedEnabled = true)
val viewModel = FeedViewModel(mockRepository, flags)
viewModel.loadFeed()
verify(mockRepository).getPersonalizedFeed()
}
@Test
fun `shows chronological feed when new algorithm disabled`() {
val flags = TestFeatureFlags(isNewFeedEnabled = false)
val viewModel = FeedViewModel(mockRepository, flags)
viewModel.loadFeed()
verify(mockRepository).getChronologicalFeed()
}
Key Takeaways
| Concept | Rule |
|---|---|
| Abstract flags | Create a FeatureFlags interface; never scatter getBoolean calls in business logic |
| Test overrides | TestFeatureFlags in unit tests; no network calls in tests |
| Kill switch | Every major feature should have one; practice the incident response flow |
| Fetch interval | 1 hour in production; 0 in debug for rapid iteration |
| Gradual rollout | Start at 1%; monitor crash rates and ANRs before increasing |