DEV Community

Adham Lou
Adham Lou

Posted on

Feature Flags in Android with Jetpack Compose

Feature flags are an essential tool for controlling feature rollouts, enabling A/B testing, and managing experimental features without requiring a new app release. In this guide, we’ll implement feature flags in an Android app using Jetpack Compose, DataStore Preferences, and app flavors.

Why Use Feature Flags?

  • Feature flags allow you to:
  • Enable or disable features dynamically
  • Deploy incomplete features safely
  • Roll out features to specific user groups
  • Experiment with new ideas without affecting all users
  • Now, let's walk through the implementation!

Step 1: Setting Up App Flavors

We need to define app flavors in the build to manage different feature flag configurations based on the app version (e.g., free, paid, beta).gradle file:

android {
    flavorDimensions "version"
    productFlavors {
        free {
            dimension "version"
        }
        paid {
            dimension "version"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This setup allows us to maintain separate feature flag configurations for each flavor.

Step 2: Adding DataStore for Persistent Storage

Jetpack DataStore (Preferences) is ideal for storing feature flags because it is lightweight and supports asynchronous data reading and writing.

First, add the dependency to your build.gradle:

dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

Next, create a DataStore helper class to manage feature flags:

private val Context.dataStore by preferencesDataStore(name = "feature_flags")

class FeatureFlagManager(
     private val context: Context
) {

    companion object {
        val FEATURE_X = booleanPreferencesKey("feature_x")
    }

    val isFeatureXEnabled: Flow<Boolean> = context.dataStore.data
        .map { preferences -> preferences[FEATURE_X] ?: false }

    suspend fun setFeatureX(enabled: Boolean) {
        context.dataStore.edit { it[FEATURE_X] = enabled }
    }
}
Enter fullscreen mode Exit fullscreen mode

This class provides a Flow to observe feature flag changes and a function to update flags.

Step 3: Providing Different Feature Flags for Each Flavor

We’ll create separate JSON files for each flavor to predefine feature flags.

free/src/main/assets/feature_flags.json

{
  "feature_x": false
}
Enter fullscreen mode Exit fullscreen mode

paid/src/main/assets/feature_flags.json

{
  "feature_x": true
}
Enter fullscreen mode Exit fullscreen mode

Then, we can load these values at app startup.

fun Context.loadFeatureFlagsFromAssets(): Map<String, Boolean> {
    val assetName = "feature_flags.json"
    return try {
        val inputStream: InputStream = assets.open(assetName)
        val json = inputStream.bufferedReader().use { it.readText() }
        val jsonObject = JSONObject(json)
        jsonObject.keys().asSequence().associateWith { jsonObject.getBoolean(it) }
    } catch (e: Exception) {
        emptyMap()
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Using Feature Flags in Jetpack Compose

We can now use the FeatureFlagManager to control UI elements dynamically in

@Composable
fun FeatureXScreen(featureFlagManager: FeatureFlagManager) {
    val isFeatureXEnabled by featureFlagManager.isFeatureXEnabled.collectAsState(initial = false)

    Column {
        Text("Feature X is ${if (isFeatureXEnabled) "Enabled" else "Disabled"}")
        if (isFeatureXEnabled) {
            Button(onClick = { /* Do something */ }) {
                Text("Try Feature X")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

By implementing feature flags with DataStore and app flavors, we gain greater flexibility in managing features dynamically. This approach enables safer releases and better control over feature rollouts. 🚀

Top comments (1)

Collapse
 
yessine_jawa_apachi profile image
Yessine Jawa

Thank you for sharing this !