A/B testing lets you make data-driven product decisions. Remote Config powers both feature flags (instant rollback) and experiment configuration (different variants for different users).
Firebase Remote Config Setup
// build.gradle.kts
implementation("com.google.firebase:firebase-config-ktx")
// Initialize with defaults (always set defaults — they apply when offline)
class RemoteConfigManager @Inject constructor(
private val config: FirebaseRemoteConfig
) {
init {
config.setDefaultsAsync(
mapOf(
"checkout_button_color" to "blue",
"homepage_hero_variant" to "control",
"max_feed_items" to 20L,
"enable_dark_mode_prompt" to false,
"onboarding_steps_count" to 3L
)
)
config.setConfigSettingsAsync(
remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
// 0 in debug = fetch every time; 3600 in prod = at most once per hour
}
)
}
suspend fun fetchAndActivate(): Boolean = config.fetchAndActivate().await()
}
Reading Config Values
class FeatureConfig @Inject constructor(private val config: FirebaseRemoteConfig) {
val checkoutButtonColor: String get() = config.getString("checkout_button_color")
val homepageHeroVariant: String get() = config.getString("homepage_hero_variant")
val maxFeedItems: Int get() = config.getLong("max_feed_items").toInt()
val isDarkModePromptEnabled: Boolean get() = config.getBoolean("enable_dark_mode_prompt")
}
// Use in Compose
@Composable
fun CheckoutButton(config: FeatureConfig, onClick: () -> Unit) {
val color = when (config.checkoutButtonColor) {
"green" -> Color(0xFF4CAF50)
"orange" -> Color(0xFFFF9800)
else -> MaterialTheme.colorScheme.primary // "blue" = default
}
Button(onClick = onClick, colors = ButtonDefaults.buttonColors(containerColor = color)) {
Text("Checkout")
}
}
A/B Test: Full Setup
// 1. Set conditions in Firebase Console:
// Condition "Group A" → Random percentile 0–49 → checkout_button_color = "green"
// Condition "Group B" → Random percentile 50–99 → checkout_button_color = "blue"
// 2. Track the conversion metric (checkout_completed) segmented by variant
class CheckoutViewModel @Inject constructor(
private val featureConfig: FeatureConfig,
private val analytics: AnalyticsTracker
) : ViewModel() {
init {
// Log which variant this user sees (for segmenting analytics)
analytics.setUserProperty(
"checkout_button_variant",
featureConfig.checkoutButtonColor
)
}
fun onCheckoutCompleted(orderId: String) {
analytics.track(AnalyticsEvent.CheckoutCompleted(
orderId = orderId,
buttonVariant = featureConfig.checkoutButtonColor // in event params
))
}
}
Remote Config Fetch Strategy
class AppStartup @Inject constructor(
private val remoteConfigManager: RemoteConfigManager
) {
suspend fun initialize() {
// Fetch in background at startup; don't block UI
withContext(Dispatchers.IO) {
try {
remoteConfigManager.fetchAndActivate()
} catch (e: Exception) {
// Use defaults; don't crash startup
}
}
}
}
Feature Flags (Kill Switches)
class FeatureFlags @Inject constructor(private val config: FirebaseRemoteConfig) {
val isNewSearchEnabled: Boolean get() = config.getBoolean("new_search_enabled")
val isCheckoutV2Enabled: Boolean get() = config.getBoolean("checkout_v2_enabled")
}
// In UI
@Composable
fun SearchScreen(flags: FeatureFlags, viewModel: SearchViewModel) {
if (flags.isNewSearchEnabled) {
NewSearchBar(viewModel)
} else {
LegacySearchBar(viewModel)
}
}
A/B Test Lifecycle
1. Hypothesis: "Green checkout button will increase conversions by 10%"
2. Setup: Remote Config condition + analytics metric
3. Run: 2+ weeks, min 1000 users per variant
4. Analyze: checkout_completed rate per variant (control vs treatment)
5. Decision:
- p-value < 0.05 AND improvement > threshold → ship winning variant
- No significant difference → keep control (simpler is better)
- Negative result → roll back treatment
6. Clean up: remove the flag; hardcode the winner
Key Takeaways
| Pattern | Rule |
|---|---|
| Always set defaults | Remote Config works offline using defaults |
| Fetch interval | 3600s in production (Firebase enforces min interval); 0 in debug |
| Log variant as user property | Enables cohort analysis in Firebase Analytics |
| Statistical significance | Need p < 0.05 AND practical significance before calling a winner |
| Clean up flags | A feature flag is temporary code — delete it after the experiment ends |
| Kill switch naming | Prefix with enable_ → easy to scan in Firebase Console |