Skip to content

Android SDK Reference

TL;DR: Add io.featureflip:featureflip-android, call client.boolVariation("flag-key", false), and you’re evaluating flags from any Android app — Kotlin-first, lifecycle-aware.

The Featureflip Android SDK is a Kotlin-first, client-side library for evaluating feature flags in Android applications. Like the other client SDKs, it receives pre-evaluated flag values from the Featureflip Evaluation API rather than running an evaluator on-device — your targeting rules never ship inside your APK or AAB. The SDK is published to Maven Central as io.featureflip:featureflip-android and works with any Android project on minSdk 24+ (Android 7.0).

The SDK is built around kotlinx.coroutines end-to-end. Initialization is a suspend function so you can await it from a lifecycleScope or viewModelScope, and flag updates are exposed as StateFlows that integrate cleanly with Jetpack Compose, View Binding, and Data Binding. A composable extension function lets you read a flag’s current value from a Compose function and re-render automatically when percentage rollouts shift the assignment. The streaming connection is automatically paused when your activity goes to the background and resumed when it returns to the foreground, so you don’t burn battery on idle devices.

For offline starts, the SDK transparently caches the latest snapshot to disk via DataStore. If the user opens your app in airplane mode or on a flaky network, targeting rules still resolve to the values from the last successful sync, and the SDK quietly retries the connection in the background. The companion Java SDK shares the underlying evaluation engine, so flag values for the same context are guaranteed identical across your Android client and your JVM backend services.

View source on GitHub

Add the dependency to your build.gradle.kts:

dependencies {
implementation("io.featureflip:featureflip-android:1.0.0")
}

The FeatureflipConfig data class accepts the following options:

OptionTypeDefaultDescription
clientKeyStringrequiredClient SDK key from your project settings
baseUrlString"https://eval.featureflip.io"Evaluation API base URL
contextMap<String, String>emptyMap()Initial evaluation context (user attributes)
streamingBooleantrueEnable SSE for real-time flag updates
pollIntervalMsLong30_000Polling interval in milliseconds (used when streaming is disabled)
flushIntervalMsLong30_000Interval in milliseconds between event flush batches
flushBatchSizeInt100Maximum number of events per flush batch
initTimeoutMsLong10_000Maximum time in milliseconds to wait for initial flag fetch
val config = FeatureflipConfig(
clientKey = "your-client-sdk-key",
context = mapOf("user_id" to "u-123"),
streaming = true,
pollIntervalMs = 60_000,
initTimeoutMs = 15_000,
)

Obtain a client via the static factory FeatureflipClient.get(config). There is no public constructor — the factory dedupes by client key, so every call with the same key returns a handle pointing at one shared underlying client. See Lifetime for the full contract.

import dev.featureflip.android.FeatureflipClient
import dev.featureflip.android.FeatureflipConfig
val config = FeatureflipConfig(clientKey = "your-client-sdk-key")
val client = FeatureflipClient.get(config)
client.initialize()

initialize() loads any cached flags from disk, makes a synchronous fetch for fresh flag values from the API, then starts SSE streaming (or polling) and the event processor. The SDK is usable with cached values even if the network request fails. Call initialize() from a background thread to avoid blocking the main thread.

Access from anywhere:

val enabled = FeatureflipClient.get(config).boolVariation("my-feature", false)

Calling get() again with the same clientKey returns a new handle pointing at the cached shared core — no new HTTP request, no new SSE connection. Only the first call does the real work.

The Android SDK is singleton-by-construction. You cannot new a client directly, and every FeatureflipClient.get(config) call with the same clientKey returns a handle pointing at one shared background client. This is intentional — a Featureflip client maintains a long-lived SSE streaming connection, a periodic event flush loop, a disk cache, and a lifecycle observer. Opening more than one per client key per process wastes battery, bandwidth, and causes flag updates to fan out unpredictably.

The factory refcounts the shared core using AtomicInteger:

  • The first get() for a given client key constructs the shared core and returns a handle with refcount 1.
  • Subsequent get() calls with the same key return a new handle and atomically increment the refcount. The config argument on repeat calls is ignored; if the baseUrl / streaming / intervals differ meaningfully, a warning is logged.
  • handle.close() decrements the refcount. When the refcount reaches zero, the shared core runs its real shutdown: stops streaming/polling, removes the lifecycle observer, flushes pending events, and removes itself from the factory cache.
  • Double-closing the same handle is a no-op — the handle tracks its own disposed state via AtomicBoolean and will not double-decrement the refcount.
  • A different client key always constructs an independent shared core.

This makes the SDK safe under any lifetime pattern:

  • Application-level singleton (the recommended pattern): call get() once in your Application.onCreate(), inject the handle everywhere, close() it in onTerminate() (if you care about clean shutdown in tests).
  • Per-Activity or per-ViewModel access: calling get() in each Activity’s onCreate() or each ViewModel constructor is harmless. Each call returns a fresh handle on the same underlying shared core. Close the handle in the corresponding onDestroy() / ViewModel onCleared() to decrement the refcount; the core stays alive as long as at least one handle references it.
  • DI container misregistration: Hilt/Koin/Dagger scoped or transient bindings become harmless. Each resolution allocates a handle, not a new HTTP client or SSE connection.

Use FeatureflipClient.forTesting(overrides) to obtain a standalone test client that is not registered in the factory cache — each call returns an independent instance with no background connections.

client.isInitialized: Boolean

A read-only property that returns true once initialize() has completed (regardless of whether the network fetch succeeded — cached flags may be in use).

All variation methods are synchronous and return immediately from an in-memory snapshot. They accept a flag key and a default value. The default is returned if the flag is missing or has an unexpected type.

Since this is a client-side SDK, evaluation context is set at initialization or via identify() — it is not passed per evaluation call.

val enabled = client.boolVariation("dark-mode", false)
val color = client.stringVariation("button-color", "blue")
val limit = client.numberVariation("rate-limit", 100.0)

Returns a Double.

val config = client.jsonVariation("ui-config", null)

Returns Any? — the raw deserialized value.

Call identify() to re-evaluate all flags for a new user context — typically after login. This makes a network request to the evaluation API with the new context and updates all cached flag values.

client.identify(mapOf("user_id" to "u-123", "plan" to "pro"))

The streaming or polling data source is also updated with the new context.

Enqueues a custom analytics event. Events are batched and flushed automatically.

client.track("purchase-completed", mapOf(
"plan" to "pro",
"amount" to 29.99,
))

Forces all buffered events to be sent immediately.

client.flush()

Decrements the refcount on the shared core. When the last handle for a given client key is closed, the shared core stops streaming/polling, removes the lifecycle observer, flushes remaining events, and removes itself from the factory cache. Double-close on the same handle is a no-op.

client.close()

On Android with androidx.lifecycle:lifecycle-process on the classpath, the SDK automatically observes app lifecycle events:

  • Background: Stops streaming/polling and flushes events
  • Foreground: Resumes streaming/polling

No manual intervention is required.

Creates a test client with static flag overrides. No network calls are made and no background tasks are started. The client is immediately initialized.

val client = FeatureflipClient.forTesting(mapOf(
"dark-mode" to true,
"pricing-tier" to "enterprise",
"rate-limit" to 500,
))
client.boolVariation("dark-mode", false) // true
client.stringVariation("pricing-tier", "free") // "enterprise"
client.boolVariation("unknown", false) // false (default)

Signature:

fun forTesting(overrides: Map<String, Any?>): FeatureflipClient

Supported override value types: Boolean, String, Int, Double.

The internal representation of a flag returned by the server:

PropertyTypeDescription
valueAny?The evaluated flag value
variationStringThe key of the matched variation
reasonStringWhy this value was returned
ValueDescription
"Fallthrough"No rules matched; the fallthrough variation was served
"RuleMatch"A targeting rule matched the context
"FlagDisabled"The flag is disabled; the off variation was served
"FlagNotFound"The flag key does not exist
"Error"An error occurred during evaluation
"TEST"Returned by test clients
PlatformMinimum Version
Android API21
Kotlin1.9+
Java17+