-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add kotlin-example module using android-sdk-framework directly #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: snapshot/typo/apr-17
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Add project specific ProGuard rules here. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
|
||
| <uses-permission android:name="android.permission.INTERNET" /> | ||
|
|
||
| <application | ||
| android:name=".EppoApplication" | ||
| android:allowBackup="true" | ||
| android:label="@string/app_name" | ||
| android:theme="@style/Theme.KotlinExample"> | ||
|
|
||
| <activity | ||
| android:name=".MainActivity" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
| </application> | ||
| </manifest> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<JsonElement> { | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔥 |
||||||||||||
|
|
||||||||||||
| 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) | ||||||||||||
|
||||||||||||
| throw ConfigurationParseException("Failed to parse JSON value: $jsonValue", e) | |
| throw ConfigurationParseException( | |
| "Failed to parse JSON value (length=${jsonValue.length})", | |
| e, | |
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<TextView>(R.id.statusText) | ||
| val assignmentText = findViewById<TextView>(R.id.assignmentText) | ||
|
|
||
| try { | ||
| val client = BaseAndroidClient.getInstance<JsonElement>() | ||
| 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 = "" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <androidx.constraintlayout.widget.ConstraintLayout | ||
| xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="match_parent" | ||
| android:padding="24dp"> | ||
|
|
||
| <TextView | ||
| android:id="@+id/titleText" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:text="@string/app_name" | ||
| android:textSize="24sp" | ||
| android:textStyle="bold" | ||
| android:layout_marginTop="48dp" | ||
| app:layout_constraintTop_toTopOf="parent" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" /> | ||
|
|
||
| <TextView | ||
| android:id="@+id/statusText" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:text="@string/status_loading" | ||
| android:textSize="16sp" | ||
| android:layout_marginTop="24dp" | ||
| app:layout_constraintTop_toBottomOf="@id/titleText" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" /> | ||
|
|
||
| <TextView | ||
| android:id="@+id/assignmentText" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:textSize="14sp" | ||
| android:layout_marginTop="16dp" | ||
| app:layout_constraintTop_toBottomOf="@id/statusText" | ||
| app:layout_constraintStart_toStartOf="parent" | ||
| app:layout_constraintEnd_toEndOf="parent" /> | ||
|
|
||
| </androidx.constraintlayout.widget.ConstraintLayout> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <color name="purple_200">#FFBB86FC</color> | ||
| <color name="purple_500">#FF6200EE</color> | ||
| <color name="purple_700">#FF3700B3</color> | ||
| <color name="teal_200">#FF03DAC5</color> | ||
| <color name="teal_700">#FF018786</color> | ||
| <color name="black">#FF000000</color> | ||
| <color name="white">#FFFFFFFF</color> | ||
| </resources> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <string name="app_name">Eppo Kotlin Example</string> | ||
| <string name="status_loading">Loading…</string> | ||
| <string name="status_ready">Client ready</string> | ||
| <string name="status_not_initialized">Client not initialized yet</string> | ||
| <string name="assignment_result">Assignment: %s</string> | ||
| </resources> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <resources> | ||
| <style name="Theme.KotlinExample" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> | ||
| <item name="colorPrimary">@color/purple_500</item> | ||
| <item name="colorPrimaryVariant">@color/purple_700</item> | ||
| <item name="colorOnPrimary">@color/white</item> | ||
| <item name="colorSecondary">@color/teal_200</item> | ||
| <item name="colorSecondaryVariant">@color/teal_700</item> | ||
| <item name="colorOnSecondary">@color/black</item> | ||
| <item name="android:statusBarColor">?attr/colorPrimaryVariant</item> | ||
| </style> | ||
| </resources> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Gradle script loads
local.propertiesvianew FileInputStream(...)but never closes the stream. In long-lived Gradle daemons this can leak file descriptors. UselocalPropertiesFile.withInputStream { localProperties.load(it) }(or a try-with-resources equivalent) to ensure the stream is closed.