androidengineers.Book a session

Monitoring & Analytics

User Behavior, A/B & Remote Config

article20 minMedium

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

PatternRule
Always set defaultsRemote Config works offline using defaults
Fetch interval3600s in production (Firebase enforces min interval); 0 in debug
Log variant as user propertyEnables cohort analysis in Firebase Analytics
Statistical significanceNeed p < 0.05 AND practical significance before calling a winner
Clean up flagsA feature flag is temporary code — delete it after the experiment ends
Kill switch namingPrefix with enable_ → easy to scan in Firebase Console

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
User Behavior, A/B & Remote Config | Android System Design | Android Engineers