> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appdna.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Experiments

> A/B test with deterministic, offline-capable bucketing

<Info>
  **Supported on:** Android SDK `1.0.33+`
</Info>

The experiments module assigns users to variants using a deterministic hash. No server call is needed — assignment is instant, works offline, and the same user always gets the same variant.

## Two ways to run an experiment

* **Servable surface experiments** — A/B test a whole paywall, onboarding flow, in-app message, or survey with **no branching code**. You design the variants in the Console and the SDK serves the right one automatically. Best for testing the content or design of a managed surface.
* **Code-level experiments** — read the assigned variant yourself with [`getVariant`](#get-variant) or [`getExperimentConfig`](#get-a-per-variant-config-value) and branch in your own code. Best for feature flags, custom UI, or logic that isn't a managed surface.

## Servable Surface Experiments

<Info>
  **Supported on:** Android SDK `1.0.37+`
</Info>

Run an A/B test on a live paywall, onboarding flow, in-app message, or survey **without changing your app code**. In the Console, create an experiment on the surface: the **control** variant points at the live entity you already present, and the **treatment** variant carries an alternate configuration.

When your app presents that surface the way it normally does, the SDK does the rest:

```kotlin theme={null}
// Present the live paywall exactly as before — no experiment code here.
AppDNA.presentPaywall(activity = activity, id = "premium_paywall")
```

The SDK deterministically buckets the user and:

* **Treatment cohort** → renders the treatment's configuration in place of the live entity.
* **Control cohort** (and users on older SDK versions) → renders the live entity unchanged.

Exposure is tracked automatically the first time the surface is presented, so conversions attribute to the experiment with no extra calls. The treatment configuration lives only inside the experiment payload, so a user who isn't bucketed into the treatment — or who is on an SDK version that predates this feature — can never receive it.

This works for every managed surface: **paywalls, onboarding flows, in-app messages, and surveys**. You only reach for the code-level API below when you're experimenting on something that isn't a managed surface.

## Get Variant

```kotlin theme={null}
val variant = AppDNA.experiments.getVariant("paywall-test")

when (variant) {
    "control" -> showStandardPaywall()
    "variant_a" -> showNewPaywall()
    else -> showStandardPaywall() // Always handle the default case
}
```

Returns the variant string or `null` if the experiment is not found, not running, or the user is not in the target audience. The convenience method `AppDNA.getExperimentVariant("paywall-test")` is equivalent.

<Warning>
  Always handle the `null` / `else` case. Experiments can be archived or stopped at any time from the Console.
</Warning>

## Check a Specific Variant

```kotlin theme={null}
val isInVariantB = AppDNA.isInVariant(
    experimentId = "paywall-test",
    variantId = "b",
)
```

Returns `true` only if the user is currently bucketed into the named variant. Useful as a guard before rendering variant-specific UI.

## Get a Per-Variant Config Value

When an experiment carries a config payload, read individual keys with:

```kotlin theme={null}
val headline = AppDNA.getExperimentConfig(
    experimentId = "paywall-test",
    key = "headline",
) as? String ?: "Get Premium"
```

Returns `Any?` (string, number, boolean, list, or map depending on what the Console stored).

## Exposure Tracking

The SDK records experiment exposures automatically the first time `getVariant()` is called for a given experiment in a session. Exposures are tracked as part of the standard analytics pipeline and surface in the Console experiment results.

To inspect the active exposures (useful for debugging or attaching them to your own analytics events):

```kotlin theme={null}
val exposures = AppDNA.experiments.getExposures()
for (entry in exposures) {
    Log.d("Exp", "${entry.experimentId} → ${entry.variant}")
}
```

Returns `List<ExposureEntry>` where each entry has `experimentId: String` and `variant: String`.

## Module Access

```kotlin theme={null}
val experiments = AppDNA.experiments
```

### Module Methods

| Method         | Signature                                   | Description                                             |
| -------------- | ------------------------------------------- | ------------------------------------------------------- |
| `getVariant`   | `getVariant(experimentId: String): String?` | Get the assigned variant, or `null` if not eligible     |
| `getExposures` | `getExposures(): List<ExposureEntry>`       | Snapshot of all active exposures (experiment → variant) |

The top-level `AppDNA` object also exposes `AppDNA.getExperimentVariant(experimentId)`, `AppDNA.isInVariant(experimentId, variantId)`, and `AppDNA.getExperimentConfig(experimentId, key)` as convenience methods that delegate to this module.

## Experiment Lifecycle

| Status        | `getVariant()` returns                               |
| ------------- | ---------------------------------------------------- |
| **Draft**     | `null` — not visible to SDKs                         |
| **Running**   | Assigned variant string                              |
| **Completed** | Winning variant for all users (server-side decision) |
| **Archived**  | `null` — removed from config                         |

## Full Example

```kotlin theme={null}
import ai.appdna.sdk.AppDNA
import ai.appdna.sdk.paywalls.PaywallContext
import android.app.Activity

class PaywallExperiment(private val activity: Activity) {

    fun showPaywall() {
        val variant = AppDNA.experiments.getVariant("paywall_redesign")

        val paywallId = when (variant) {
            "new_design" -> "paywall_v2"
            else -> "paywall_v1"
        }

        AppDNA.presentPaywall(
            activity = activity,
            id = paywallId,
            context = PaywallContext(
                placement = "settings",
                experiment = "paywall_redesign",
                variant = variant ?: "control",
            ),
        )
    }
}
```

<Info>
  Experiments are created in the Console under **Experiments**. The SDK uses MurmurHash3 for deterministic assignment — the same user always gets the same variant across sessions, platforms, and even offline.
</Info>

## Next Steps

* Use [Feature Flags](/sdks/android/feature-flags) for typed boolean rollouts
* Combine experiments with [Remote Config](/sdks/android/remote-config) for variant-scoped values
* Present [Paywalls](/sdks/android/paywall) using `PaywallContext` to attribute conversions to the experiment
