Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 11 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<String, Any>(
"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/).
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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<View>(R.id.retry_button).setOnClickListener { loadDatafile() }

loadDatafile()
}

Expand All @@ -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<String, Any>(
"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<Map<String, Any>>(
"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) }
Expand All @@ -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()
Expand All @@ -99,7 +123,10 @@ class MainActivity : Activity() {
loadedF: Featurevisor,
enabled: Boolean,
variation: String?,
welcomeMessage: String?,
maxItems: Int?,
paymentMethods: List<String>?,
endpoints: Map<String, Any>?,
supportContact: String?,
) {
if (isFinishing || isDestroyed) {
loadedF.close()
Expand All @@ -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
Expand All @@ -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"
}
}
48 changes: 45 additions & 3 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/mobile_experience_key"
android:text="@string/results_title"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
Expand Down Expand Up @@ -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" />

<TextView
android:id="@+id/variable_value"
android:id="@+id/max_items_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="18sp" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/payment_methods_label"
android:textColor="@color/text_secondary" />

<TextView
android:id="@+id/payment_methods_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="18sp" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/service_endpoint_label"
android:textColor="@color/text_secondary" />

<TextView
android:id="@+id/service_endpoint_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
android:textSize="18sp" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/support_contact_label"
android:textColor="@color/text_secondary" />

<TextView
android:id="@+id/support_contact_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/text_primary"
Expand Down
9 changes: 6 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
<resources>
<string name="app_name">Featurevisor Example</string>
<string name="title">Featurevisor for Android</string>
<string name="subtitle">Evaluating mobile_experience with the Featurevisor SDK.</string>
<string name="subtitle">Evaluating features and global variables with the Featurevisor SDK.</string>
<string name="loading_datafile">Loading datafile…</string>
<string name="try_again">Try again</string>
<string name="unknown_error">An unknown error occurred.</string>
<string name="no_value">none</string>
<string name="mobile_experience_key">mobile_experience</string>
<string name="results_title">SDK evaluation results</string>
<string name="flag_label">Flag</string>
<string name="variation_label">Variation</string>
<string name="variable_label">Welcome message variable</string>
<string name="max_items_label">Maximum checkout items</string>
<string name="payment_methods_label">Payment methods</string>
<string name="service_endpoint_label">Service endpoint</string>
<string name="support_contact_label">Support contact</string>
<string name="enabled">Enabled</string>
<string name="disabled">Disabled</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Any>(
"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<Map<String, Any>>("serviceEndpoints")["baseUrl"],
)
assertEquals("support-nl@example.com", f.getVariableString("supportContact"))
} finally {
f.close()
}
Expand Down
1 change: 0 additions & 1 deletion app/src/test/resources/featurevisor-mobile.json

This file was deleted.

Loading