From 179b473e4a90f28067619d41704a68ccdf07cb4a Mon Sep 17 00:00:00 2001 From: Fahad Heylaal Date: Sat, 29 Aug 2026 18:49:55 +0200 Subject: [PATCH] chore: upgrade to v4 --- README.md | 46 +++---------- app/build.gradle.kts | 2 +- .../example/android/MainActivity.kt | 68 +++++++++++++------ app/src/main/res/layout/activity_main.xml | 48 ++++++++++++- app/src/main/res/values/strings.xml | 9 ++- .../example/android/FeaturevisorTest.kt | 35 +++++----- .../test/resources/featurevisor-mobile.json | 1 - .../test/resources/featurevisor-sdk-v3.json | 1 + 8 files changed, 131 insertions(+), 79 deletions(-) delete mode 100644 app/src/test/resources/featurevisor-mobile.json create mode 100644 app/src/test/resources/featurevisor-sdk-v3.json diff --git a/README.md b/README.md index 51c7c72..34747a8 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,43 @@ # featurevisor-example-android -A small Android application written entirely in Kotlin, showing how to use the [Featurevisor SDK](https://github.com/featurevisor/featurevisor-java). +A small Android application written entirely in Kotlin, showing how to use the [Featurevisor Java SDK](https://github.com/featurevisor/featurevisor-java). -The application downloads a Featurevisor datafile, creates an SDK instance, and evaluates the `mobile_experience` feature as a flag, variation, and string variable. +Learn more about Featurevisor [here](https://featurevisor.com). + +The application evaluates the same flag, variation, feature variables, and global variables as the other Featurevisor SDK examples. ## Requirements Install Android Studio with Android SDK 36 and JDK 17. -The project uses the Kotlin support built into Android Gradle Plugin 9, so it does not require the older Kotlin Android Gradle plugin. - -The Featurevisor SDK is published through GitHub Packages. Create a GitHub personal access token with the `read:packages` scope, then add these values to `~/.gradle/gradle.properties`: +The Featurevisor SDK is published through GitHub Packages. Add GitHub credentials with the `read:packages` scope to `~/.gradle/gradle.properties`: ```properties gpr.user=YOUR_GITHUB_USERNAME gpr.key=YOUR_GITHUB_TOKEN ``` -Keep these credentials outside the repository. For GitHub Actions, grant the example repository access to the Featurevisor SDK package. +Keep these credentials outside the repository. ## Run the application 1. Open the repository in Android Studio. -2. Allow Gradle to sync and resolve the Featurevisor dependency. +2. Allow Gradle to resolve the Featurevisor dependency. 3. Select an Android emulator or connected device. 4. Run the `app` configuration. -## Featurevisor usage - -The application fetches this production datafile: +The application uses this production datafile: ```text -https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json +https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-sdk-v3.json ``` -The important SDK usage is in `MainActivity.kt`: - -```kotlin -val datafile = DatafileContent.fromJson(datafileJson) -val f = Featurevisor.createFeaturevisor( - Featurevisor.FeaturevisorOptions().datafile(datafile), -) - -val context = mapOf( - "userId" to "mobile-user", - "country" to "nl", -) - -val enabled = f.isEnabled("mobile_experience", context) -val variation = f.getVariation("mobile_experience", context) -val message = f.getVariableString( - "mobile_experience", - "welcome_message", - context, -) -``` +The direct SDK integration lives in `MainActivity.kt`. Change its context values to see how Featurevisor selects different rules, variations, and global variable overrides. ## Checks -Run the unit tests, Android lint, and a debug build with: - ```sh ./gradlew testDebugUnitTest lintDebug assembleDebug ``` -Learn more in the [Featurevisor SDK documentation](https://featurevisor.com/docs/sdks/java/). +Learn more in the [Featurevisor Java SDK documentation](https://featurevisor.com/docs/sdks/java/). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 166a22f..813cf1e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,7 +25,7 @@ android { } dependencies { - implementation("com.featurevisor:featurevisor-java:3.0.0") + implementation("com.featurevisor:featurevisor-java:4.0.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") diff --git a/app/src/main/kotlin/com/featurevisor/example/android/MainActivity.kt b/app/src/main/kotlin/com/featurevisor/example/android/MainActivity.kt index 4fe7ba4..a967468 100644 --- a/app/src/main/kotlin/com/featurevisor/example/android/MainActivity.kt +++ b/app/src/main/kotlin/com/featurevisor/example/android/MainActivity.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.TextView import com.featurevisor.sdk.DatafileContent import com.featurevisor.sdk.Featurevisor +import com.featurevisor.sdk.FeaturevisorLogLevel import java.net.HttpURLConnection import java.net.URL import java.util.concurrent.Executors @@ -20,7 +21,10 @@ class MainActivity : Activity() { private lateinit var errorMessage: TextView private lateinit var flagValue: TextView private lateinit var variationValue: TextView - private lateinit var variableValue: TextView + private lateinit var maxItemsValue: TextView + private lateinit var paymentMethodsValue: TextView + private lateinit var serviceEndpointValue: TextView + private lateinit var supportContactValue: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -32,10 +36,12 @@ class MainActivity : Activity() { errorMessage = findViewById(R.id.error_message) flagValue = findViewById(R.id.flag_value) variationValue = findViewById(R.id.variation_value) - variableValue = findViewById(R.id.variable_value) + maxItemsValue = findViewById(R.id.max_items_value) + paymentMethodsValue = findViewById(R.id.payment_methods_value) + serviceEndpointValue = findViewById(R.id.service_endpoint_value) + supportContactValue = findViewById(R.id.support_contact_value) findViewById(R.id.retry_button).setOnClickListener { loadDatafile() } - loadDatafile() } @@ -45,24 +51,44 @@ class MainActivity : Activity() { executor.execute { try { val datafile = DatafileContent.fromJson(fetchDatafile()) - val loadedF = Featurevisor.createFeaturevisor( - Featurevisor.FeaturevisorOptions().datafile(datafile), - ) val context = mapOf( - "userId" to "mobile-user", + "userId" to "customer-123", "country" to "nl", + "locale" to "nl-NL", + "accountPlan" to "pro", + ) + val loadedF = Featurevisor.createFeaturevisor( + Featurevisor.FeaturevisorOptions() + .datafile(datafile) + .context(context) + .logLevel(FeaturevisorLogLevel.ERROR), ) - val enabled = loadedF.isEnabled("mobile_experience", context) - val variation = loadedF.getVariation("mobile_experience", context) - val welcomeMessage = loadedF.getVariableString( - "mobile_experience", - "welcome_message", - context, + val enabled = loadedF.isEnabled("commerce_platform") + val variation = loadedF.getVariation("checkout_experience") + val maxItems = loadedF.getVariableInteger( + "checkout_experience", + "max_items", ) + val paymentMethods = loadedF.getVariableArray( + "checkout_experience", + "payment_methods", + ) + val endpoints = loadedF.getVariableObject>( + "serviceEndpoints", + ) + val supportContact = loadedF.getVariableString("supportContact") runOnUiThread { - showResults(loadedF, enabled, variation, welcomeMessage) + showResults( + loadedF, + enabled, + variation, + maxItems, + paymentMethods, + endpoints, + supportContact, + ) } } catch (exception: Exception) { runOnUiThread { showError(exception.message) } @@ -78,11 +104,9 @@ class MainActivity : Activity() { return try { val statusCode = connection.responseCode - if (statusCode !in 200..299) { error("Datafile request failed with HTTP $statusCode.") } - connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() } } finally { connection.disconnect() @@ -99,7 +123,10 @@ class MainActivity : Activity() { loadedF: Featurevisor, enabled: Boolean, variation: String?, - welcomeMessage: String?, + maxItems: Int?, + paymentMethods: List?, + endpoints: Map?, + supportContact: String?, ) { if (isFinishing || isDestroyed) { loadedF.close() @@ -111,7 +138,10 @@ class MainActivity : Activity() { flagValue.setText(if (enabled) R.string.enabled else R.string.disabled) variationValue.text = variation ?: getString(R.string.no_value) - variableValue.text = welcomeMessage ?: getString(R.string.no_value) + maxItemsValue.text = maxItems?.toString() ?: getString(R.string.no_value) + paymentMethodsValue.text = paymentMethods?.joinToString() ?: getString(R.string.no_value) + serviceEndpointValue.text = endpoints?.get("baseUrl")?.toString() ?: getString(R.string.no_value) + supportContactValue.text = supportContact ?: getString(R.string.no_value) loadingView.visibility = View.GONE errorView.visibility = View.GONE resultsView.visibility = View.VISIBLE @@ -136,6 +166,6 @@ class MainActivity : Activity() { private companion object { const val DATAFILE_URL = - "https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-mobile.json" + "https://featurevisor-example-cloudflare.pages.dev/production/featurevisor-sdk-v3.json" } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2ab264e..853a505 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -77,7 +77,7 @@ @@ -114,11 +114,53 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:text="@string/variable_label" + android:text="@string/max_items_label" android:textColor="@color/text_secondary" /> + + + + + + + + + + + + Featurevisor Example Featurevisor for Android - Evaluating mobile_experience with the Featurevisor SDK. + Evaluating features and global variables with the Featurevisor SDK. Loading datafile… Try again An unknown error occurred. none - mobile_experience + SDK evaluation results Flag Variation - Welcome message variable + Maximum checkout items + Payment methods + Service endpoint + Support contact Enabled Disabled diff --git a/app/src/test/kotlin/com/featurevisor/example/android/FeaturevisorTest.kt b/app/src/test/kotlin/com/featurevisor/example/android/FeaturevisorTest.kt index 536ca05..28c51d5 100644 --- a/app/src/test/kotlin/com/featurevisor/example/android/FeaturevisorTest.kt +++ b/app/src/test/kotlin/com/featurevisor/example/android/FeaturevisorTest.kt @@ -8,34 +8,35 @@ import org.junit.Test class FeaturevisorTest { @Test - fun evaluatesMobileExperience() { + fun evaluatesFeaturesAndGlobalVariables() { val json = requireNotNull( - javaClass.classLoader?.getResource("featurevisor-mobile.json"), + javaClass.classLoader?.getResource("featurevisor-sdk-v3.json"), ).readText() - val f = Featurevisor.createFeaturevisor( - Featurevisor.FeaturevisorOptions().datafile( - DatafileContent.fromJson(json), - ), - ) val context = mapOf( - "userId" to "mobile-user", + "userId" to "customer-123", "country" to "nl", + "locale" to "nl-NL", + "accountPlan" to "pro", + ) + val f = Featurevisor.createFeaturevisor( + Featurevisor.FeaturevisorOptions() + .datafile(DatafileContent.fromJson(json)) + .context(context), ) try { - assertTrue(f.isEnabled("mobile_experience", context)) + assertTrue(f.isEnabled("commerce_platform")) + assertEquals("express", f.getVariation("checkout_experience")) + assertEquals(25, f.getVariableInteger("checkout_experience", "max_items")) assertEquals( - "treatment", - f.getVariation("mobile_experience", context), + listOf("card", "wallet"), + f.getVariableArray("checkout_experience", "payment_methods"), ) assertEquals( - "Welkom", - f.getVariableString( - "mobile_experience", - "welcome_message", - context, - ), + "https://api.eu.example.com", + f.getVariableObject>("serviceEndpoints")["baseUrl"], ) + assertEquals("support-nl@example.com", f.getVariableString("supportContact")) } finally { f.close() } diff --git a/app/src/test/resources/featurevisor-mobile.json b/app/src/test/resources/featurevisor-mobile.json deleted file mode 100644 index f8dfc24..0000000 --- a/app/src/test/resources/featurevisor-mobile.json +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion":"2","featurevisorVersion":"3.2.0","revision":"14","segments":{"netherlands":{"conditions":"[{\"attribute\":\"country\",\"operator\":\"equals\",\"value\":\"nl\"}]"},"supportedMobileApp":{"conditions":"[{\"attribute\":\"appVersion\",\"operator\":\"semverGreaterThanOrEquals\",\"value\":\"2.0.0\"}]"}},"features":{"mobile_experience":{"bucketBy":"userId","variations":[{"value":"control","weight":50,"variables":{"welcome_message":"Welcome","show_new_navigation":false}},{"value":"treatment","weight":50,"variables":{"welcome_message":"Welkom","show_new_navigation":true}}],"traffic":[{"key":"everyone","segments":"*","percentage":100000,"allocation":[{"variation":"control","range":[0,50000]},{"variation":"treatment","range":[50000,100000]}]}],"variablesSchema":{"welcome_message":{"type":"string","defaultValue":"Welcome"},"show_new_navigation":{"type":"boolean","defaultValue":false}},"force":[{"segments":"netherlands","enabled":true,"variation":"treatment"},{"conditions":"[{\"attribute\":\"country\",\"operator\":\"equals\",\"value\":\"de\"}]","enabled":true,"variation":"control"}],"hash":"VY7LVbM5cG"},"mobile_feed":{"bucketBy":"userId","traffic":[{"key":"supported-apps","segments":"supportedMobileApp","percentage":100000,"variables":{"page_size":20,"layout":"cards","sections":["featured","news","recommended"],"card_style":{"cornerRadius":16,"showImage":true}}},{"key":"older-apps","segments":"*","percentage":25000}],"variablesSchema":{"page_size":{"type":"integer","defaultValue":10},"layout":{"type":"string","defaultValue":"compact"},"sections":{"type":"array","defaultValue":["news"]},"card_style":{"type":"object","defaultValue":{"cornerRadius":8,"showImage":false}}},"hash":"UxSC0lb2VE"},"mobile_theme":{"bucketBy":"userId","variations":[{"value":"light","weight":50},{"value":"contrast","weight":50}],"traffic":[{"key":"everyone","segments":"*","percentage":100000,"allocation":[{"variation":"light","range":[0,50000]},{"variation":"contrast","range":[50000,100000]}]}],"hash":"RXTQul8KCB"}}} \ No newline at end of file diff --git a/app/src/test/resources/featurevisor-sdk-v3.json b/app/src/test/resources/featurevisor-sdk-v3.json new file mode 100644 index 0000000..2dc430b --- /dev/null +++ b/app/src/test/resources/featurevisor-sdk-v3.json @@ -0,0 +1 @@ +{"schemaVersion":"2","featurevisorVersion":"3.7.0","revision":"15","segments":{"premiumAccounts":{"conditions":"[{\"attribute\":\"accountPlan\",\"operator\":\"in\",\"value\":[\"pro\",\"enterprise\"]}]"}},"features":{"checkout_experience":{"bucketBy":"userId","requiredFeatures":["commerce_platform"],"variations":[{"value":"standard","weight":50,"variables":{"checkout_mode":"standard","max_items":10,"payment_methods":["card"],"presentation":{"accent":"blue","showProgress":false}}},{"value":"express","weight":50,"variables":{"checkout_mode":"express","max_items":25,"payment_methods":["card","wallet"],"presentation":{"accent":"green","showProgress":true}}}],"traffic":[{"key":"premium-accounts","segments":"premiumAccounts","percentage":100000,"allocation":[{"variation":"standard","range":[0,50000]},{"variation":"express","range":[50000,100000]}]},{"key":"gradual-rollout","segments":"*","percentage":70000,"allocation":[{"variation":"standard","range":[0,35000]},{"variation":"express","range":[35000,70000]}]}],"variablesSchema":{"checkout_mode":{"type":"string","defaultValue":"standard"},"max_items":{"type":"integer","defaultValue":10},"payment_methods":{"type":"array","defaultValue":["card"]},"presentation":{"type":"object","defaultValue":{"accent":"blue","showProgress":false}}},"force":[{"segments":"premiumAccounts","enabled":true,"variation":"express"},{"conditions":"[{\"attribute\":\"country\",\"operator\":\"equals\",\"value\":\"nl\"}]","enabled":true,"variation":"express"}],"hash":"G8x8YLBx4S"},"commerce_platform":{"bucketBy":"userId","traffic":[{"key":"premium-accounts","segments":"premiumAccounts","percentage":100000},{"key":"gradual-rollout","segments":"*","percentage":80000}],"force":[{"conditions":"[{\"attribute\":\"accountPlan\",\"operator\":\"equals\",\"value\":\"disabled\"}]","enabled":false},{"segments":"premiumAccounts","enabled":true},{"conditions":"[{\"attribute\":\"country\",\"operator\":\"equals\",\"value\":\"nl\"}]","enabled":true}],"hash":"QbyP3IoHAA"}},"variables":{"serviceEndpoints":{"type":"object","defaultValue":{"baseUrl":"https://api.example.com","timeoutMs":3000,"retries":2},"overrides":[{"key":"premium","value":{"baseUrl":"https://api.eu.example.com","timeoutMs":1200,"retries":4},"keyPath":["europe","premium"],"conditions":"{\"attribute\":\"country\",\"operator\":\"in\",\"value\":[\"nl\",\"de\",\"fr\"]}","segments":"premiumAccounts"},{"key":"europe","value":{"baseUrl":"https://api.eu.example.com","timeoutMs":2500,"retries":2},"conditions":"{\"attribute\":\"country\",\"operator\":\"in\",\"value\":[\"nl\",\"de\",\"fr\"]}"},{"key":"default","value":{"baseUrl":"https://api.example.com","timeoutMs":3000,"retries":2},"segments":"*"}],"hash":"vJCOzkvpiu"},"supportContact":{"type":"string","defaultValue":"support@example.com","overrides":[{"key":"dutch","value":"support-nl@example.com","conditions":"{\"attribute\":\"locale\",\"operator\":\"equals\",\"value\":\"nl-NL\"}"},{"key":"default","value":"support@example.com","segments":"*"}],"hash":"6pGU1l8aap"}}}