Android Quickstart
Get Running with the Convert Android SDK in 5 Minutes
THIS ARTICLE WILL HELP YOU:
- Get an Overview
- Add Dependency
- Initialize Once
- Wait for Config, Then Run an Experience
- Create a Visitor Context
- Run an Experience
- Track Conversions and Revenue
- Understand Implementation Considerations
- Know Offline Behavior
- Track Control and Consent
- Get QA Checklist
- Troubleshoot
- Conclusion
Overview
The Convert Android SDK lets you run Convert Full Stack experiments and feature flags inside native Android applications. It is designed for app-side experimentation where the app decides which screen, component, copy, layout, pricing treatment, onboarding flow, or feature experience a visitor should receive.
You need an SDK Key from your Convert dashboard and an Android project with minSdk 24 or higher.
Convert Full Stack projects allow experimentation across application layers, including frontend/app UI and backend logic, so teams can optimize the full user experience rather than only browser DOM changes.
1. Add the Dependency
Declare Maven Central and Google’s Maven repository in settings.gradle.kts, then add the SDK dependency to your app module.
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
}
}
// app/build.gradle.kts
dependencies {
implementation("com.convert:sdk-android:+") // pin a specific version in production
}
The Android SDK is published to Maven Central as com.convert:sdk-android. The + resolves to the latest published version, but Convert recommends pinning a specific version in production so builds remain reproducible.
Toolchain Requirements
Use the following minimum requirements for the Android SDK:
|
Toolchain |
Minimum |
|
JDK |
17 |
|
Android runtime API / minSdk |
24 |
|
compileSdk |
35 recommended |
|
Kotlin |
2.x |
|
Gradle |
8.x |
|
Android Gradle Plugin |
8.x or newer |
The SDK targets minSdk 24. Building against a lower minSdk will fail, so raise your app module’s minSdk to at least 24.
Permissions
The SDK requires no special permission to function. The INTERNET permission is merged automatically from the library manifest.
For best offline recovery behavior, add ACCESS_NETWORK_STATE to your app’s AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
This lets the SDK observe connectivity changes and flush queued events immediately when the device reconnects. If the permission is not present, the SDK degrades gracefully and queued events still flush on the next scheduled batch.
2. Initialize Once in Application.onCreate()
Create exactly one SDK instance per process, preferably in your Application class. The SDK owns a long-lived coroutine scope, an offline queue, WorkManager registration, and network observers, so multiple instances can waste resources and may create duplicate tracking events.
import android.app.Application
import com.convert.sdk.android.ConvertSDK
import com.convert.sdk.core.model.LogLevel
class MyApp : Application() {
lateinit var convertSdk: ConvertSDK
override fun onCreate() {
super.onCreate()
convertSdk = ConvertSDK.builder(this)
.sdkKey("YOUR_SDK_KEY") // from your Convert dashboard
.logLevel(LogLevel.INFO) // use DEBUG while integrating
.build()
}
}
Register the class in AndroidManifest.xml:
<application
android:name=".MyApp"
... />
ConvertSDK has no public constructor. Every instance should be created through ConvertSDK.builder(context).build().
3. Wait for Config, Then Run an Experience
The SDK fetches its bucketing config asynchronously. Use onReady { ... } before your first decision.
val sdk = (application as MyApp).convertSdk
sdk.onReady {
val ctx = sdk.createContext() // auto-persisted UUID visitor ID
val variation = ctx.runExperience("homepage-redesign")
when (variation?.key) {
"control" -> renderControl()
"treatment" -> renderTreatment()
null -> renderControl() // not ready or visitor not bucketed
}
}
onReady dispatches on Dispatchers.Default, so switch back to the main thread before touching Android UI elements.
Example using the main thread:
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
val sdk = (application as MyApp).convertSdk
sdk.onReady {
val ctx = sdk.createContext()
val variation = ctx.runExperience("homepage-redesign")
CoroutineScope(Dispatchers.Main).launch {
when (variation?.key) {
"control" -> renderControl()
"treatment" -> renderTreatment()
else -> renderControl()
}
}
}
4. Create a Visitor Context
A ConvertContext binds one visitor to the SDK. You can create one using an auto-generated visitor ID, an explicit visitor ID, or an explicit visitor ID with attributes.
// 1. Auto visitor — UUID v4 persisted in SharedPreferences
val ctx = sdk.createContext()
// 2. Explicit visitor — use your own stable ID, such as a logged-in user ID
val ctx = sdk.createContext("visitor-42")
// 3. Explicit visitor with initial attributes
val ctx = sdk.createContext(
visitorId = "visitor-42",
attributes = mapOf(
"plan" to "premium",
"accountAgeDays" to 120
)
)
The auto visitor ID is persisted in SharedPreferences and remains stable across app launches. Explicit visitor IDs are useful when you want to align Convert bucketing with your own logged-in user identity.
5. Running an Experience
Use runExperience() with the experience key configured in Convert:
val variation = ctx.runExperience("homepage-redesign")
Then use the returned variation to control app behavior:
when (variation?.key) {
"new-onboarding" -> showNewOnboarding()
"control" -> showOriginalOnboarding()
else -> showOriginalOnboarding()
}
This pattern is useful when the variation should change a native screen, onboarding step, layout, message, pricing treatment, or app-side feature flow.
6. Tracking Conversions and Revenue
Use trackConversion() to send goal completions to Convert.
Bare conversion:
ctx.trackConversion("signup-completed")
Conversion with transactional goal data:
import com.convert.sdk.core.model.GoalData
import com.convert.sdk.core.model.GoalDataKey
import kotlinx.serialization.json.JsonPrimitive
ctx.trackConversion(
goalKey = "purchase-completed",
goalData = listOf(
GoalData(key = GoalDataKey.AMOUNT, value = JsonPrimitive(49.99)),
GoalData(key = GoalDataKey.TRANSACTION_ID, value = JsonPrimitive("tx-42")),
),
)
Use conversion tracking when a visitor completes an important app action, such as signing up, purchasing, starting a trial, finishing onboarding, activating a feature, or reaching a milestone.
trackConversion is fire-and-forget. Events are batched, persisted to disk, and flushed in the background. They can survive an offline device and an app restart.
7. App UI-Facing Implementation Considerations
The Android SDK makes the experiment decision inside the native app. Your UI implementation should make that decision visible consistently and avoid screen flicker or conflicting assignment.
Pattern A: Native UI Decision After SDK Readiness
Use this when the app can wait for SDK readiness before rendering the experiment-controlled screen.
sdk.onReady {
val ctx = sdk.createContext()
val variation = ctx.runExperience("checkout-screen-test")
CoroutineScope(Dispatchers.Main).launch {
when (variation?.key) {
"simplified-checkout" -> showSimplifiedCheckout()
"control" -> showStandardCheckout()
else -> showStandardCheckout()
}
}
}
This is the cleanest pattern for screens where you can show a loading state, splash screen, or defer rendering until the SDK has config.
Pattern B: Safe Default First, Then Apply Variation
Use this when the screen must render immediately. Show the control experience first, then apply the treatment only after the SDK is ready.
showStandardCheckout()
sdk.onReady {
val ctx = sdk.createContext()
val variation = ctx.runExperience("checkout-screen-test")
CoroutineScope(Dispatchers.Main).launch {
if (variation?.key == "simplified-checkout") {
showSimplifiedCheckout()
}
}
}
This is simple, but it can create a visible UI change if the treatment is applied after the user has already seen the control. For important first impressions, onboarding, checkout, pricing, or paywall tests, prefer gating the screen until the SDK is ready.
Pattern C: Backend + Android App Consistency
Use this when a backend service and the Android app both need to know the same experiment assignment.
Recommended approach:
- Use the same stable visitor ID in the backend and Android app.
- Keep one source of truth for assignment when possible.
- Pass the backend decision to the app if the backend is controlling entitlement, pricing, or API behavior.
- Avoid running the same experiment independently in both backend and Android unless the IDs and bucketing logic are intentionally aligned.
Convert Full Stack experimentation supports testing across frontend and backend components, including application UI and underlying business logic.
Avoid Duplicate Bucketing
Do not evaluate the same experiment in multiple places using different visitor IDs.
Common causes of inconsistent app experiences include:
- using auto-generated anonymous IDs in the app but logged-in IDs on the backend
- creating more than one SDK instance per process
- calling experiment logic before SDK readiness and not handling null
- showing a cached UI state that does not match the current visitor context
- re-bucketing the user after login without planning the anonymous-to-logged-in transition
Use a stable visitor ID when possible, and create exactly one SDK instance per process.
8. Offline Behavior
The Android SDK is built to preserve tracking events on flaky or absent networks. It uses on-device durable storage, WorkManager, and connectivity callbacks.
When the network is unavailable:
- Decision or conversion events are queued.
- Failed or offline flushes are persisted to app-private storage.
- WorkManager schedules retry work.
- When connectivity returns, the SDK drains the persisted queue and flushes events.
- The queue can survive app restarts and device reboots.
The SDK stores the offline event queue, cached config, and visitor state inside the app’s private sandbox, and these are removed on app uninstall.
You do not need to manually flush events, manually refresh config, or manually register WorkManager. The SDK handles batching, foreground/background handoff, and retry behavior.
9. Tracking Control and Consent
The Android SDK supports tracking control through two mechanisms:
- SDK-level tracking toggle
- Per-call tracking override
Use the SDK-level toggle when consent is unknown or withdrawn:
sdk.setTrackingEnabled(false) // consent withdrawn or unknown
sdk.setTrackingEnabled(true) // consent granted
val current = sdk.isTrackingEnabled()
You can also start the SDK in silent mode:
val sdk = ConvertSDK.builder(context)
.sdkKey("YOUR_SDK_KEY")
.trackingEnabled(false) // start silent until consent resolves
.build()
Use a per-call override when you want to evaluate a decision but suppress outbound tracking for that specific call:
val variation = ctx.runExperience(
"homepage-redesign",
enableTracking = false
)
ctx.runExperiences(enableTracking = false)
When tracking is disabled, bucketing and rule evaluation can still work, but outbound tracking events are not sent. Events generated while tracking is disabled are not buffered for later replay.
10. QA Checklist
Before launching your Android SDK implementation, verify the following:
- The SDK dependency is added from Maven Central.
- Production builds pin a specific SDK version instead of using +.
- The app uses minSdk 24 or higher.
- The SDK is initialized once in Application.onCreate().
- The application class is registered in AndroidManifest.xml.
- onReady { ... } is used before the first experience decision.
- UI updates inside onReady are marshaled back to the main thread.
- Visitor IDs are stable across app launches and login states.
- Experience keys match the keys configured in Convert.
- Goal keys match the goals configured in Convert.
- Purchase values and transaction IDs are passed correctly.
- ACCESS_NETWORK_STATE is declared for best offline flush behavior.
- Consent logic uses .trackingEnabled(false) or setTrackingEnabled(false) where required.
- QA logs are checked with LogLevel.DEBUG.
11. Troubleshooting
Decisions keep returning null
Confirm that you are waiting for onReady { ... } before calling runExperience(). Until the first config loads, runExperience can return null.
sdk.onReady {
val ctx = sdk.createContext()
val variation = ctx.runExperience("homepage-redesign")
}
UI crashes or behaves unexpectedly after onReady
onReady dispatches on Dispatchers.Default. Switch to the main thread before touching Android views, fragments, activities, or Compose state.
Visitor sees different variations after login
This can happen if the app uses an anonymous auto-generated visitor ID before login, then switches to a different logged-in visitor ID. Decide how you want to handle anonymous-to-logged-in identity before launch.
For logged-in experiences, use an explicit stable visitor ID:
val ctx = sdk.createContext("user-123")
Conversions are not appearing
Check that:
- the SDK is ready
- tracking is enabled
- the goal key is correct
- the app has network connectivity
- events are not being suppressed with enableTracking = false
- the visitor was actually exposed to the relevant experience
Events are batched and flushed in the background, and offline events can be persisted and retried.
Offline events do not flush immediately after reconnect
Add ACCESS_NETWORK_STATE to the manifest for best reconnect behavior:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Without this permission, the SDK still degrades gracefully and queued events flush on the next scheduled batch, but the immediate reconnect flush optimization is skipped.
Need to verify the integration in Logcat
Set the SDK log level to DEBUG while integrating:
convertSdk = ConvertSDK.builder(this)
.sdkKey("YOUR_SDK_KEY")
.logLevel(LogLevel.DEBUG)
.build()
Then watch Logcat:
adb logcat -s ConvertSDK:V
A successful integration should log the config fetch and fire the ready event.
Conclusion
The Convert Android SDK lets you run native app experiments and feature flags directly inside Android applications. Add the SDK dependency, initialize one SDK instance in Application.onCreate(), wait for onReady, create a visitor context, run experiences, and track conversions.
For Android UI-facing experiments, decide whether to gate rendering until the SDK is ready or render a safe default first. Keep visitor IDs stable, avoid duplicate bucketing, and account for consent and offline behavior before launch.