Kotlin Multiplatform SDK for Amplitude Experiment (feature flags & A/B testing).
It mirrors the public API of the official experiment-android-client / experiment-jvm-server
SDKs — same com.amplitude.experiment package and type names — so migrating an existing Android
or JVM project is (mostly) a matter of swapping the dependency. All evaluation logic is provided by
the shared, platform-agnostic evaluation engine; this SDK contains only the Experiment client/server
code, not a reimplementation of the engine.
| Module | Android | JVM | iOS | Linux x64 |
|---|---|---|---|---|
core |
✅ | ✅ | ✅ | ✅ |
client |
✅ | ✅ | ✅ | — |
server |
— | ✅ | — | ✅ |
- core — shared public types:
ExperimentUser,Variant,ServerZone. - client — remote evaluation for client apps: fetch variants, local caching, exposure tracking.
- server — local evaluation for backends, using the shared evaluation engine.
dependencies {
// Client SDK (Android / JVM / iOS)
implementation("me.nathanfallet.amplitude:experiment-kmp-client:<version>")
// Server SDK (JVM / Linux)
implementation("me.nathanfallet.amplitude:experiment-kmp-server:<version>")
}The evaluation engine is pulled in transitively from
me.nathanfallet.amplitude:evaluation-core
— a fork of Amplitude's evaluation-core with iOS targets added.
The API is coroutine-based (suspend), which is idiomatic for Kotlin Multiplatform. Note the Android
SDK's Future-based async API is JVM-only and therefore cannot be reproduced verbatim in common code.
import com.amplitude.experiment.Experiment
import com.amplitude.experiment.ExperimentConfig
import com.amplitude.experiment.ExperimentUser
import com.amplitude.experiment.ServerZone
val config = ExperimentConfig.builder()
.debug(true)
.serverZone(ServerZone.US)
.build()
// suspend
val client = Experiment.initialize(apiKey = "your-deployment-key", config = config)
val user = ExperimentUser.builder()
.userId("user-id")
.deviceId("device-id")
.userProperty("premium", true)
.build()
// Fetch variants from the server (suspend)
client.fetch(user)
// Read a variant (with optional fallback)
val variant = client.variant("flag-key", Variant(value = "control"))
if (variant.value == "treatment") {
// ...
}
// All variants
val all = client.all()On Android, provide the platform Context via the config so variants are cached in
SharedPreferences:
val config = ExperimentConfig.builder()
.androidContext(applicationContext)
.build()Storage per platform: Android → SharedPreferences, iOS → NSUserDefaults, JVM → Preferences.
import com.amplitude.experiment.LocalEvaluation
import com.amplitude.experiment.LocalEvaluationConfig
import com.amplitude.experiment.ExperimentUser
val config = LocalEvaluationConfig.builder()
.debug(true)
.flagConfigPollerIntervalMillis(30_000)
.build()
// suspend
val client = LocalEvaluation.initialize(apiKey = "your-server-key", config = config)
// Fetch flag configs and start polling
client.start()
val user = ExperimentUser.builder().userId("user-id").build()
// Evaluate locally — targeting, bucketing and flag dependencies are all applied
val variant = client.evaluate(user, "flag-key")
val all = client.evaluateAll(user)
client.stop()- No duplicated evaluation logic. Flag configurations are deserialized directly into the engine's
EvaluationFlagtype and evaluated byevaluation-core. The server mapsExperimentUserinto the engine's nestedEvaluationContextand converts results back into the publicVariant. - Platform code via
expect/actual— only storage and the HTTP engine are platform-specific.
./gradlew build # build + test everything
./gradlew :server:jvmTest
./gradlew :client:testDebugUnitTestRequirements: JDK 17+, Kotlin 2.3, Android SDK (for Android targets), a macOS host (for iOS targets).
- ✅ Client (Android / JVM / iOS) and server (JVM / Linux) compile, link and test.
- ✅ Correct server-side local evaluation (targeting / bucketing / dependencies), covered by tests.
- ✅ API parity with
experiment-android-client(flatcom.amplitude.experimentpackage, config options, logging vialogLevel/loggerProvider,customRequestHeaders). The async API issuspend(KMP-idiomatic) rather than Android's JVM-onlyFuture. - ✅
initializeWithAmplitudeAnalytics— full Amplitude Analytics bridge on Android and iOS (since 1.0.1): identity sync + exposure forwarding through the shared native analytics connector (the same one the Amplitude Analytics SDK / amplitude-kmp writes to). In 1.0.0 it delegated toinitialize. On iOS this pulls in theAnalyticsConnectorCocoaPod — see the note below. - ⏳ Cohort sync (server) — not yet implemented; flags targeting cohorts won't match without it.
The iOS analytics bridge integrates the @objc AnalyticsConnector pod via Kotlin/Native cinterop.
The published artifact ships Kotlin Multiplatform klibs (consumed from another KMP project — no
XCFramework/SPM/podspec is published), and the iOS klib carries a cinterop dependency on the
AnalyticsConnector pod. So an iOS consumer must provide that pod at link time — regardless of
which code path runs (the connector code is inert at runtime if you only call Experiment.initialize,
but it is still a link input). The pod already ships with AmplitudeSwift / amplitude-kmp, so if you
use Amplitude Analytics on iOS you already have it.
Building/publishing the iOS targets therefore requires a macOS host with the CocoaPods CLI installed.
MIT License — this project ports/wraps Amplitude's MIT-licensed SDKs. Copyright (c) Amplitude Inc. and contributors.