diff --git a/android-sdk-framework/src/main/java/cloud/eppo/android/framework/storage/CachingConfigurationStore.java b/android-sdk-framework/src/main/java/cloud/eppo/android/framework/storage/CachingConfigurationStore.java
index 2b83b4f..e1488b3 100644
--- a/android-sdk-framework/src/main/java/cloud/eppo/android/framework/storage/CachingConfigurationStore.java
+++ b/android-sdk-framework/src/main/java/cloud/eppo/android/framework/storage/CachingConfigurationStore.java
@@ -85,7 +85,7 @@ protected CachingConfigurationStore(
*
* @param config the configuration to seed (must not be null)
*/
- void seedCache(@NotNull Configuration config) {
+ public void seedCache(@NotNull Configuration config) {
if (config == null) {
throw new IllegalArgumentException("config must not be null");
}
diff --git a/build.gradle b/build.gradle
index fa71816..4dd0a87 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,8 @@
plugins {
id 'com.android.application' version '8.10.1' apply false
id 'com.android.library' version '8.10.1' apply false
+ id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
+ id 'org.jetbrains.kotlin.plugin.serialization' version '2.0.21' apply false
}
task clean(type: Delete) {
diff --git a/kotlin-example/build.gradle b/kotlin-example/build.gradle
new file mode 100644
index 0000000..7e42e56
--- /dev/null
+++ b/kotlin-example/build.gradle
@@ -0,0 +1,65 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.plugin.serialization'
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localProperties.load(new FileInputStream(localPropertiesFile))
+}
+
+android {
+ namespace 'cloud.eppo.kotlinexample'
+ compileSdk 34
+
+ buildFeatures {
+ buildConfig true
+ }
+
+ defaultConfig {
+ applicationId 'cloud.eppo.kotlinexample'
+ minSdk 26
+ targetSdk 34
+ versionCode 1
+ versionName '1.0'
+
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+
+ buildConfigField "String", "EPPO_API_KEY",
+ "\"" + (localProperties['cloud.eppo.apiKey'] ?: "set-eppo-api-key-in-local.properties") + "\""
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ signingConfig signingConfigs.debug
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation project(':android-sdk-framework')
+ // sdk-common-jvm:4.0.0-SNAPSHOT is the split version compatible with eppo-sdk-framework:0.1.0-SNAPSHOT.
+ // 3.13.1 is the old monolithic version that bundles framework classes — it conflicts with
+ // eppo-sdk-framework (which comes transitively from :android-sdk-framework).
+ implementation 'cloud.eppo:sdk-common-jvm:4.0.0-SNAPSHOT'
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
+ implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
+ implementation 'org.slf4j:slf4j-api:1.7.36'
+ runtimeOnly 'org.slf4j:slf4j-android:1.7.36'
+ implementation 'androidx.appcompat:appcompat:1.7.1'
+ implementation 'com.google.android.material:material:1.12.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
+}
diff --git a/kotlin-example/proguard-rules.pro b/kotlin-example/proguard-rules.pro
new file mode 100644
index 0000000..fb164d6
--- /dev/null
+++ b/kotlin-example/proguard-rules.pro
@@ -0,0 +1 @@
+# Add project specific ProGuard rules here.
diff --git a/kotlin-example/src/main/AndroidManifest.xml b/kotlin-example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..57ba968
--- /dev/null
+++ b/kotlin-example/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/EppoApplication.kt b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/EppoApplication.kt
new file mode 100644
index 0000000..bae4c22
--- /dev/null
+++ b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/EppoApplication.kt
@@ -0,0 +1,37 @@
+package cloud.eppo.kotlinexample
+
+import android.app.Application
+import android.util.Log
+import cloud.eppo.OkHttpEppoClient
+import cloud.eppo.android.framework.BaseAndroidClient
+
+class EppoApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ initEppoClient()
+ }
+
+ private fun initEppoClient() {
+ BaseAndroidClient.Builder(
+ BuildConfig.EPPO_API_KEY,
+ this,
+ KotlinxConfigurationParser(),
+ OkHttpEppoClient()
+ )
+ .isGracefulMode(true)
+ .buildAndInitAsync()
+ .handle { _, ex ->
+ if (ex != null) {
+ Log.e(TAG, "Eppo initialization failed", ex)
+ } else {
+ Log.i(TAG, "Eppo client initialized")
+ }
+ null
+ }
+ }
+
+ companion object {
+ private const val TAG = "EppoApplication"
+ }
+}
diff --git a/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/KotlinxConfigurationParser.kt b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/KotlinxConfigurationParser.kt
new file mode 100644
index 0000000..8f448ca
--- /dev/null
+++ b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/KotlinxConfigurationParser.kt
@@ -0,0 +1,37 @@
+package cloud.eppo.kotlinexample
+
+import cloud.eppo.JacksonConfigurationParser
+import cloud.eppo.api.dto.BanditParametersResponse
+import cloud.eppo.api.dto.FlagConfigResponse
+import cloud.eppo.parser.ConfigurationParseException
+import cloud.eppo.parser.ConfigurationParser
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+
+/**
+ * [ConfigurationParser] implementation that uses kotlinx.serialization for JSON flag values.
+ *
+ * Flag configuration and bandit parameter parsing is delegated to [JacksonConfigurationParser]
+ * because the framework DTO types ([FlagConfigResponse], [BanditParametersResponse]) have
+ * hand-rolled Jackson deserializers in sdk-common-jvm that handle Eppo's obfuscated config
+ * format. Only [parseJsonValue] uses kotlinx.serialization, since that is the method that
+ * returns the user-facing [JsonElement] type.
+ */
+class KotlinxConfigurationParser : ConfigurationParser {
+
+ private val delegate = JacksonConfigurationParser()
+ private val json = Json { ignoreUnknownKeys = true }
+
+ override fun parseFlagConfig(flagConfigJson: ByteArray): FlagConfigResponse =
+ delegate.parseFlagConfig(flagConfigJson)
+
+ override fun parseBanditParams(banditParamsJson: ByteArray): BanditParametersResponse =
+ delegate.parseBanditParams(banditParamsJson)
+
+ override fun parseJsonValue(jsonValue: String): JsonElement =
+ try {
+ json.parseToJsonElement(jsonValue)
+ } catch (e: Exception) {
+ throw ConfigurationParseException("Failed to parse JSON value: $jsonValue", e)
+ }
+}
diff --git a/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/MainActivity.kt b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/MainActivity.kt
new file mode 100644
index 0000000..8c07c4b
--- /dev/null
+++ b/kotlin-example/src/main/kotlin/cloud/eppo/kotlinexample/MainActivity.kt
@@ -0,0 +1,35 @@
+package cloud.eppo.kotlinexample
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import cloud.eppo.android.framework.BaseAndroidClient
+import cloud.eppo.android.framework.exceptions.NotInitializedException
+import cloud.eppo.api.Attributes
+import kotlinx.serialization.json.JsonElement
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+
+ val statusText = findViewById(R.id.statusText)
+ val assignmentText = findViewById(R.id.assignmentText)
+
+ try {
+ val client = BaseAndroidClient.getInstance()
+ val assignment = client.getStringAssignment(
+ "my-example-flag",
+ "user-123",
+ Attributes(),
+ "control"
+ )
+ statusText.text = getString(R.string.status_ready)
+ assignmentText.text = getString(R.string.assignment_result, assignment)
+ } catch (e: NotInitializedException) {
+ statusText.text = getString(R.string.status_not_initialized)
+ assignmentText.text = ""
+ }
+ }
+}
diff --git a/kotlin-example/src/main/res/layout/activity_main.xml b/kotlin-example/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..0a3dfbd
--- /dev/null
+++ b/kotlin-example/src/main/res/layout/activity_main.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/kotlin-example/src/main/res/values/colors.xml b/kotlin-example/src/main/res/values/colors.xml
new file mode 100644
index 0000000..ca1931b
--- /dev/null
+++ b/kotlin-example/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
diff --git a/kotlin-example/src/main/res/values/strings.xml b/kotlin-example/src/main/res/values/strings.xml
new file mode 100644
index 0000000..bf5d1c4
--- /dev/null
+++ b/kotlin-example/src/main/res/values/strings.xml
@@ -0,0 +1,8 @@
+
+
+ Eppo Kotlin Example
+ Loading…
+ Client ready
+ Client not initialized yet
+ Assignment: %s
+
diff --git a/kotlin-example/src/main/res/values/themes.xml b/kotlin-example/src/main/res/values/themes.xml
new file mode 100644
index 0000000..94148a0
--- /dev/null
+++ b/kotlin-example/src/main/res/values/themes.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
index a31545d..2c18a7a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -19,4 +19,5 @@ dependencyResolutionManagement {
rootProject.name = "Eppo SDK"
include ':example'
include ':eppo'
-include ':android-sdk-framework'
\ No newline at end of file
+include ':android-sdk-framework'
+include ':kotlin-example'
\ No newline at end of file