skills/feature-flags/SKILL.md
Feature flag patterns - LaunchDarkly, Firebase Remote Config, local feature flags, A/B testing, gradual rollouts, and KMP shared flag evaluation.
npx skillsauth add ahmed3elshaer/everything-claude-code-mobile feature-flagsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
interface FeatureFlagProvider {
fun getBooleanFlag(key: String, default: Boolean = false): Boolean
fun getStringFlag(key: String, default: String = ""): String
fun getIntFlag(key: String, default: Int = 0): Int
fun getDoubleFlag(key: String, default: Double = 0.0): Double
suspend fun refresh()
}
sealed class FeatureFlag<T>(
val key: String,
val defaultValue: T
) {
// Boolean flags
object NewOnboarding : FeatureFlag<Boolean>("new_onboarding_v2", false)
object DarkModeEnabled : FeatureFlag<Boolean>("dark_mode_enabled", true)
object ChatFeature : FeatureFlag<Boolean>("chat_feature", false)
// String flags
object CheckoutButtonText : FeatureFlag<String>("checkout_button_text", "Buy Now")
object HomeLayoutVariant : FeatureFlag<String>("home_layout_variant", "control")
// Numeric flags
object MaxCartItems : FeatureFlag<Int>("max_cart_items", 50)
object SearchDebounceMs : FeatureFlag<Long>("search_debounce_ms", 300L)
}
// Type-safe evaluation
class FeatureFlagManager(private val provider: FeatureFlagProvider) {
fun isEnabled(flag: FeatureFlag<Boolean>): Boolean {
return provider.getBooleanFlag(flag.key, flag.defaultValue)
}
fun getString(flag: FeatureFlag<String>): String {
return provider.getStringFlag(flag.key, flag.defaultValue)
}
fun getInt(flag: FeatureFlag<Int>): Int {
return provider.getIntFlag(flag.key, flag.defaultValue)
}
}
class FirebaseFeatureFlagProvider(context: Context) : FeatureFlagProvider {
private val remoteConfig = Firebase.remoteConfig.apply {
val configSettings = remoteConfigSettings {
minimumFetchIntervalInSeconds = if (BuildConfig.DEBUG) 0 else 3600
}
setConfigSettingsAsync(configSettings)
setDefaultsAsync(R.xml.remote_config_defaults)
}
override fun getBooleanFlag(key: String, default: Boolean): Boolean {
return remoteConfig.getBoolean(key)
}
override fun getStringFlag(key: String, default: String): String {
return remoteConfig.getString(key).ifEmpty { default }
}
override fun getIntFlag(key: String, default: Int): Int {
return remoteConfig.getLong(key).toInt()
}
override fun getDoubleFlag(key: String, default: Double): Double {
return remoteConfig.getDouble(key)
}
override suspend fun refresh() {
remoteConfig.fetchAndActivate().await()
}
}
<?xml version="1.0" encoding="utf-8"?>
<defaultsMap>
<entry>
<key>new_onboarding_v2</key>
<value>false</value>
</entry>
<entry>
<key>dark_mode_enabled</key>
<value>true</value>
</entry>
<entry>
<key>checkout_button_text</key>
<value>Buy Now</value>
</entry>
</defaultsMap>
final class FirebaseFeatureFlagProvider: FeatureFlagProvider {
private let remoteConfig = RemoteConfig.remoteConfig()
init() {
let settings = RemoteConfigSettings()
#if DEBUG
settings.minimumFetchInterval = 0
#else
settings.minimumFetchInterval = 3600
#endif
remoteConfig.configSettings = settings
remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults")
}
func getBooleanFlag(key: String, defaultValue: Bool) -> Bool {
remoteConfig.configValue(forKey: key).boolValue
}
func getStringFlag(key: String, defaultValue: String) -> String {
let value = remoteConfig.configValue(forKey: key).stringValue
return value?.isEmpty == false ? value! : defaultValue
}
func refresh() async throws {
let status = try await remoteConfig.fetchAndActivate()
print("Remote config fetch status: \(status)")
}
}
class LaunchDarklyProvider(context: Context) : FeatureFlagProvider {
private val client: LDClient
init {
val ldConfig = LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Enabled)
.mobileKey("mob-your-mobile-key")
.build()
val ldContext = LDContext.builder(ContextKind.DEFAULT, "user-id-123")
.set("email", "[email protected]")
.set("plan", "premium")
.build()
client = LDClient.init(context.applicationContext, ldConfig, ldContext, 5)
}
override fun getBooleanFlag(key: String, default: Boolean): Boolean {
return client.boolVariation(key, default)
}
override fun getStringFlag(key: String, default: String): String {
return client.stringVariation(key, default)
}
override fun getIntFlag(key: String, default: Int): Int {
return client.intVariation(key, default)
}
override fun getDoubleFlag(key: String, default: Double): Double {
return client.doubleVariation(key, default)
}
override suspend fun refresh() {
// LaunchDarkly uses streaming by default, manual refresh not needed
}
fun registerFlagChangeListener(key: String, listener: (Boolean) -> Unit) {
client.registerFeatureFlagListener(key) { flagKey ->
listener(client.boolVariation(flagKey, false))
}
}
}
// build.gradle.kts
android {
buildTypes {
debug {
buildConfigField("boolean", "ENABLE_DEV_TOOLS", "true")
buildConfigField("boolean", "MOCK_API", "true")
}
release {
buildConfigField("boolean", "ENABLE_DEV_TOOLS", "false")
buildConfigField("boolean", "MOCK_API", "false")
}
}
}
// Usage
if (BuildConfig.ENABLE_DEV_TOOLS) {
showDevMenu()
}
class LocalFeatureFlagProvider(
private val prefs: SharedPreferences
) : FeatureFlagProvider {
override fun getBooleanFlag(key: String, default: Boolean): Boolean {
return prefs.getBoolean("flag_$key", default)
}
fun overrideFlag(key: String, value: Boolean) {
prefs.edit().putBoolean("flag_$key", value).apply()
}
fun clearOverrides() {
prefs.edit().clear().apply()
}
}
// commonMain
interface SharedFeatureFlags {
fun isEnabled(key: String, default: Boolean = false): Boolean
fun getString(key: String, default: String = ""): String
}
// androidMain
class AndroidFeatureFlags(context: Context) : SharedFeatureFlags {
private val remoteConfig = Firebase.remoteConfig
override fun isEnabled(key: String, default: Boolean) = remoteConfig.getBoolean(key)
override fun getString(key: String, default: String) = remoteConfig.getString(key)
}
// iosMain
class IosFeatureFlags : SharedFeatureFlags {
private val remoteConfig = RemoteConfig.remoteConfig()
override fun isEnabled(key: String, default: Boolean) =
remoteConfig.configValue(forKey: key).boolValue
override fun getString(key: String, default: String) =
remoteConfig.configValue(forKey: key).stringValue ?: default
}
class GradualRollout(private val userId: String) {
fun isInRollout(flagKey: String, percentage: Int): Boolean {
val hash = "$flagKey-$userId".hashCode().absoluteValue
return (hash % 100) < percentage
}
}
// Server-side targeting in Firebase Remote Config:
// Condition: "10% of users" -> random percentile <= 10
// Condition: "Premium users" -> user property "tier" == "premium"
data class Experiment(
val name: String,
val variant: String // "control", "variant_a", "variant_b"
)
fun getExperimentVariant(flagManager: FeatureFlagManager): Experiment {
val variant = flagManager.getString(FeatureFlag.HomeLayoutVariant)
analytics.logEvent("experiment_assigned", mapOf(
"experiment_name" to "home_layout",
"variant" to variant
))
return Experiment("home_layout", variant)
}
data-ai
SQLDelight patterns for Kotlin Multiplatform - .sq file definitions, platform drivers, type adapters, migrations, and shared database access.
data-ai
Room database patterns for Android - entity definitions, DAO interfaces, Database class, migrations, TypeConverters, and Flow integration.
tools
Push notification patterns - FCM setup for Android, APNs for iOS, notification channels, payload handling, foreground/background behavior, and rich notifications.
content-media
Pagination patterns for mobile - Paging 3 for Android (PagingSource, RemoteMediator, LazyPagingItems), cursor-based and offset-based strategies.