> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/developers/docs/_mcp/server.

# Android SDK runtime and interaction mechanics

> Understand how the Optimization Android SDK runs shared optimization behavior in a native app, how Compose and XML Views integrations share the same client model, and how consent, state, entry resolution, tracking, preview overrides, and offline delivery work.

## Overview

Use this concept document to understand how the Optimization Android SDK runs shared optimization
behavior in a native app, how Compose and XML Views integrations share the same client model, and
how consent, state, entry resolution, tracking, preview overrides, and offline delivery work.

For step-by-step setup, see
[Integrate the Optimization Android SDK in a Jetpack Compose app](/personalization/optimization-sdk/integrate-the-optimization-android-sdk-in-a-compose-app/)
and
[Integrate the Optimization Android SDK in an Android Views app](/personalization/optimization-sdk/integrate-the-optimization-android-sdk-in-a-views-app/).
For the full Contentful entry contract, see
[Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/).

## Runtime boundary

The Android SDK is a native Kotlin Android library published as
`com.contentful.java:optimization-android`. Kotlin owns native app concerns such as persistence,
networking, lifecycle handling, Compose helpers, XML Views helpers, preview-panel UI, and app-facing
public APIs.

Shared optimization behavior runs inside a local QuickJS context. That bridge lets the Android SDK
use the same optimization, profile, consent, and event-delivery behavior as the JavaScript SDKs
while exposing a Kotlin API to the application.

Applications do not call the JavaScript layer directly. The public boundary is Kotlin:

* `OptimizationClient` is the main facade for initialization, state, optimization, tracking, and
  preview controls.
* `OptimizationRoot`, `OptimizedEntry`, `OptimizationLazyColumn`, and `ScreenTrackingEffect` provide
  Compose integration helpers.
* `OptimizationManager`, `OptimizedEntryView`, `TrackingRecyclerView`, and `ScreenTracker` provide
  XML Views integration helpers.
* `PreviewPanelConfig` wires the in-app preview panel into Compose and XML Views integrations.

This split also defines what the SDK does not own. The application still fetches Contentful entries,
manages consent UX, controls routing, decides identity policy, and renders the final UI.

## Prerequisites and runtime constraints

Decide these policies before initialization because they shape the client state the bridge receives
at startup and the events it can emit before runtime consent changes:

| Constraint           | Android behavior                                                                                                                                                                                                                                                                                                     |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Consent              | `state.consent` starts as unset unless `StorageDefaults.consent` or persisted SDK consent provides a value. Until event consent is `true`, Android/native allow-list behavior lets only `identify` and `screen` emit by default.                                                                                     |
| Configuration        | Build `OptimizationConfig` before initialization. `spaceId` is required, `environment` defaults to `"master"`, and `locale` controls the SDK Experience/event locale. Apps often pass the same string to Contentful CDA requests, but the SDK treats that as a separate app-owned locale.                            |
| Persistence consent  | Boolean `client.consent(true)` or `client.consent(false)` updates event consent and durable profile-continuity persistence consent together. Use object-form consent when event consent and durable profile continuity have separate policy decisions.                                                               |
| Allowed event types  | `OptimizationConfig.allowedEventTypes` replaces the native default pre-consent allow-list. Pass `allowedEventTypes = emptyList()` for strict opt-in before any Optimization event, or pass a narrow custom list when legal and privacy review permits specific pre-consent events.                                   |
| Storage availability | Android stores consent and, when persistence consent is `true`, profile-continuity values in `SharedPreferences`. If storage has no usable value or is cleared, the SDK starts from configured defaults and does not restore profile-continuity state from a previous process.                                       |
| Preview mode         | The preview panel is an app opt-in surface. Mount it only in debug or internal flows; opening it forces live entry updates so audience and variant overrides are visible immediately.                                                                                                                                |
| Offline behavior     | Event queues are in memory. Events queued while offline flush when connectivity returns. The SDK also tries to flush queued events when the app moves toward the background, but it does not provide a durable outbox across process death.                                                                          |
| Configured defaults  | `StorageDefaults` are startup defaults and take precedence over persisted values. If the application persists user choices, leave consent and persistence defaults unset. Restore SDK-stored consent naturally, or call `client.consent(...)` from the resolved app policy instead of seeding a choice every launch. |

## Lifecycle and coroutines

`OptimizationClient` has two phases:

| Phase         | Behavior                                                                                                                                                   |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Uninitialized | The client exists, but the bridge is not loaded. Suspend APIs throw or return safe fallbacks depending on the call, and sync APIs no-op where appropriate. |
| Initialized   | The bridge is loaded, persisted state has been merged into configuration, SDK state is available, and lifecycle/network observers are active.              |

Compose apps usually let `OptimizationRoot` create the client and call `initialize(config)`. XML
Views apps usually call `OptimizationManager.initialize(...)` from `Application.onCreate` before
reading `OptimizationManager.client` from activities or fragments. Because
`OptimizationManager.initialize(...)` initializes the client asynchronously, direct suspend API
calls from Views code must wait for `OptimizationManager.client.isInitialized.first { it }`.

`OptimizationClient` exposes async work as `suspend` functions. Call those methods from Compose
effects, View event-handler coroutine scopes, lifecycle-aware coroutines, or another app-owned
coroutine scope.

The QuickJS runtime runs on a dedicated single-thread dispatcher owned by the SDK. Application code
must use the public Kotlin APIs instead of trying to access the bridge directly.

Typical apps keep one client alive for the app process lifetime. Use `destroy()` for test teardown
or deliberate SDK reset flows.

## Configuration and locale handoff

Every Android integration builds an `OptimizationConfig`:

```kotlin
OptimizationConfig(
    spaceId = "your-space-id",
    environment = "master",
    locale = "en-US",
    logLevel = if (BuildConfig.DEBUG) OptimizationLogLevel.debug else OptimizationLogLevel.error,
)
```

Only `spaceId` is required. `environment` defaults to `"master"`. Base URL overrides belong only in
integrations that need non-default Experience API or Insights API endpoints.

Use top-level `locale` for the SDK Experience/event locale. When the application renders localized
Contentful entries, query the Contentful Delivery API by content type and slug with one app-owned
locale, then pass that fetched entry to `OptimizedEntry`, `OptimizedEntryView`, or
`resolveOptimizedEntry(...)`. The native Android SDK does not provide managed entry fetching. For
the full locale model, see
[Locale handling in the Optimization SDK Suite](/personalization/optimization-sdk/locale-handling-in-the-optimization-sdk-suite/).

## State and persistence

`OptimizationClient` publishes runtime state through Kotlin flows:

| Surface                  | Description                                                                    |
| ------------------------ | ------------------------------------------------------------------------------ |
| `state`                  | Snapshot of profile, consent, optimization readiness, and pending changes.     |
| `isInitialized`          | `true` after initialization completes.                                         |
| `selectedOptimizations`  | The selected optimization variants for the visitor.                            |
| `state.canOptimize`      | Whether selected optimization data is available for entry resolution.          |
| `optimizationPossible`   | Whether the current consent and allow-list configuration can produce variants. |
| `experienceRequestState` | Outcome of the most recent Experience API request.                             |
| `locale`                 | Current SDK locale for Experience API requests and event context.              |
| `isPreviewPanelOpen`     | `true` while the in-app preview panel is visible.                              |
| `previewState`           | Preview override state used by the in-app preview panel.                       |
| `eventStream`            | Raw event stream for debug surfaces and tests.                                 |
| `blockedEventStream`     | Events blocked by consent gating.                                              |

Compose code reads these values through `collectAsState()` or effects. XML Views code usually
collects them from lifecycle-aware coroutines.

Android also exposes the current SDK Experience/event locale as `client.locale`. Call
`client.setLocale(locale)` after initialization to update future Experience API requests and event
context.

Custom Flags use the same Core-backed model as the Web SDKs: `client.getFlag(name)` returns the
current JSON value, and `client.observeFlag(name)` returns a `StateFlow<JSONValue?>` that updates on
distinct value changes. Both one-off reads and subscriptions can emit deduplicated flag-view events
for delivered values when consent and profile state allow.

For the lower-level state model shared through the native bridge, see
[Core state management](/personalization/optimization-sdk/core-state-management/).

The SDK persists consent with `SharedPreferences`. When persistence consent is `true`, it also
persists profile-continuity values such as profile, selected changes, selected optimizations, and
the anonymous ID. `StorageDefaults` are startup defaults, not one-time seeds. During initialization,
configured defaults take precedence over persisted SDK values, so a configured consent or
persistence default can replace a visitor's stored choice on each launch.

Use `StorageDefaults(consent = true)` only when the application's policy is truly default-on at
startup. If the application persists user choices in a CMP, account setting, or app preference,
leave `StorageDefaults.consent` and `StorageDefaults.persistenceConsent` unset. Resolve the policy
at startup and call `client.consent(...)` after the client is available instead of seeding a choice
every launch. If the application relies on SDK consent storage, also leave those defaults unset so
the persisted SDK consent can restore naturally.

## Consent and event gates

Consent is a three-state value: `true`, `false`, or unset. Until consent is granted, the SDK blocks
event types that are not allow-listed. Android uses the native default allow-list when
`OptimizationConfig.allowedEventTypes` is unset:

| Consent state | Event behavior                                              |
| ------------- | ----------------------------------------------------------- |
| Unset         | `identify` and `screen` can emit; other events are blocked. |
| `true`        | All SDK event types can emit.                               |
| `false`       | `identify` and `screen` can emit; other events are blocked. |

This is the default Android/native behavior, not a universal SDK rule. `allowedEventTypes` replaces
the default allow-list. Use `allowedEventTypes = emptyList()` when no Optimization event can emit
before explicit consent, or pass a custom list when policy permits specific pre-consent events.
Allow-listed pre-consent events still mark `context.gdpr.isConsentGiven` as `false` until event
consent is explicitly `true`.

Call `client.consent(true)` when the visitor grants consent and `client.consent(false)` when the
visitor rejects it. Boolean consent controls both event emission and durable profile-continuity
persistence by default. `client.consent(false)` clears event consent and persistence consent, purges
SDK queues, and clears SDK-managed durable profile-continuity storage while leaving active in-memory
state available until the app resets or tears down the client.

Use object-form consent when event emission and durable profile continuity need separate policy
decisions. For example, `client.consent(events = true, persistence = false)` allows events while
keeping profile continuity session-only. `client.consent(events = false)` withdraws event consent
and purges SDK queues, but it does not clear persistence consent unless `persistence = false` is
also passed. For cross-SDK consent guidance, see
[Consent management in the Optimization SDK Suite](/personalization/optimization-sdk/consent-management-in-the-optimization-sdk-suite/).

## Entry optimization boundary

Android entry resolution follows the cross-SDK data model, cross-content selection, empty-variant,
and baseline-fallback behavior described in
[Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/).
The Android-specific direct method is a `suspend` function. It returns the baseline or selected
variant as a `CTEntry`:

```kotlin
val result = client.resolveOptimizedEntry(baseline = entry)

val resolvedEntry = result.entry // CTEntry
val selectedOptimization = result.selectedOptimization
val optimizationContextId = result.optimizationContextId

when {
    resolvedEntry.contentTypeId == "hero" && resolvedEntry.hasField("headline") ->
        renderHero(resolvedEntry.getField<String>("headline"))
    resolvedEntry.contentTypeId == "cta" && resolvedEntry.hasField("label") ->
        renderCta(resolvedEntry.getField<String>("label"))
    else -> renderPage(resolvedEntry.getField<String>("title"))
}
```

`contentTypeId` reads Contentful's `sys.contentType.sys.id` discriminant. It identifies the entry
shape but does not validate its fields. `hasField` checks field presence, and `getField<T>` reads a
field as the requested Kotlin type. XML Views render callbacks receive a raw entry map; convert it
with `CTEntry.from(entryMap)` to use the same accessors.

When resolving directly, omit `selectedOptimizations` to use current client state, or pass an
explicit snapshot such as `client.selectedOptimizations.value` when a custom abstraction needs
deterministic resolution. A matched resolution can return `optimizationContextId` for manual
interaction tracking. Compose `OptimizedEntry` and XML Views `OptimizedEntryView` add variant
locking, live updates, and interaction tracking around the same resolver.

## Adapter surfaces

The Android SDK exposes two public UI adapter packages over the same core client:

| App style | Initialization path              | Entry rendering path | Screen tracking path   | Scroll tracking helper   |
| --------- | -------------------------------- | -------------------- | ---------------------- | ------------------------ |
| Compose   | `OptimizationRoot`               | `OptimizedEntry`     | `ScreenTrackingEffect` | `OptimizationLazyColumn` |
| XML Views | `OptimizationManager.initialize` | `OptimizedEntryView` | `ScreenTracker`        | `TrackingRecyclerView`   |

Both adapters use the same `OptimizationClient`, persistence model, bridge runtime, event gates,
locale resolution, and preview override state. Choose the adapter that matches the UI framework of
the screen you are integrating.

## Tracking mechanics

The Android SDK emits page context events, mobile screen events, custom business events, and
Contentful entry interaction events:

| Event type | Compose path                   | XML Views path                     |
| ---------- | ------------------------------ | ---------------------------------- |
| Page       | `client.page(...)`             | `client.page(...)`                 |
| Screen     | `ScreenTrackingEffect`         | `ScreenTracker.trackScreen(...)`   |
| Event      | App-owned event handlers       | App-owned event handlers           |
| Entry view | `OptimizedEntry` view tracking | `OptimizedEntryView` view tracking |
| Entry tap  | `OptimizedEntry` tap tracking  | `OptimizedEntryView` tap tracking  |

Entry interaction tracking uses these defaults:

* Entry view and tap tracking are enabled by default in both Compose and XML Views.
* A view session qualifies after 1 second at 10% visibility and emits one start interaction.
* While the entry remains visible, the SDK measures duration without periodic interaction timers or
  emissions.
* When a qualified session ends, the SDK emits one end interaction with the same `viewId` and the
  final duration. A session that ends before the dwell threshold emits nothing.
* Backgrounding, disappearance, and unmounting end and reset active view sessions. Foregrounding or
  remounting re-evaluates visibility and starts a fresh session when the entry is still eligible.

The 10% visibility threshold and 1-second dwell are fixed. Use `OptimizationLazyColumn` in Compose
and `TrackingRecyclerView` in XML Views when view timing needs scroll-aware visibility updates.

Applications can also call `page(...)`, `track(...)`, `trackView(...)`, and `trackClick(...)`
directly when they need to emit page context, custom business events, or entry interactions from a
custom UI abstraction.

## Live updates and preview behavior

`OptimizedEntry` and `OptimizedEntryView` lock to the first resolved variant by default. Locking
prevents content from changing while a visitor is reading it. Enable live updates when a component
needs to react to profile changes or preview overrides without a reload.

Android live-update precedence is:

| Preview panel | Global default | Per-entry override | Result |
| ------------- | -------------- | ------------------ | ------ |
| Open          | Any            | Any                | Live   |
| Closed        | `true`         | `null`             | Live   |
| Closed        | `false`        | `true`             | Live   |
| Closed        | `true`         | `false`            | Locked |
| Closed        | `false`        | `null`             | Locked |

When the preview panel is open, all `OptimizedEntry` and `OptimizedEntryView` components update live
so audience and variant overrides apply immediately. When the panel closes, non-live optimized
entries without an explicit selected-optimizations override lock to the previewed selection. Live
entries continue to follow current `selectedOptimizations`, and Views entries with caller-supplied
selected optimizations continue resolving from that explicit value.

Compose apps mount the preview panel through
`OptimizationRoot(previewPanel = PreviewPanelConfig(...))`. XML Views apps mount the Activity
floating entry point by calling `OptimizationManager.attachPreviewPanel(...)`.
`OptimizationManager.initialize(...)` receives `PreviewPanelConfig` so the manager can retain the
optional preview Contentful client used for preview-definition fetching.

## Offline and app lifecycle delivery

The Android SDK monitors network reachability and app lifecycle events:

* When the device is offline, events queue in memory.
* When connectivity returns, queued events flush automatically.
* When the app moves toward the background, the SDK flushes queued events to reduce data loss.

No configuration is required for this behavior. Queueing and flushing use the same event-delivery
model for Compose and XML Views integrations.

## Related documentation

* [Optimization Android SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/android/README.md) - Package installation, quick
  start, and published package status.
* [Consent management in the Optimization SDK Suite](/personalization/optimization-sdk/consent-management-in-the-optimization-sdk-suite/) -
  Cross-SDK consent, persistence consent, allow-list, and withdrawal policy guidance.
* [Locale handling in the Optimization SDK Suite](/personalization/optimization-sdk/locale-handling-in-the-optimization-sdk-suite/) -
  How SDK Experience/event locales relate to Contentful entry locales.
* [Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/) -
  Contentful entry contract, variant fallback behavior, and local resolution mechanics.
* [Core state management](/personalization/optimization-sdk/core-state-management/) - Shared state, consent, persistence, event
  queues, and observable mechanics used through the native bridge.
* [Integrate the Optimization Android SDK in a Jetpack Compose app](/personalization/optimization-sdk/integrate-the-optimization-android-sdk-in-a-compose-app/) -
  Compose setup flow for `OptimizationRoot`, `OptimizedEntry`, screen tracking, and preview panel
  mounting.
* [Integrate the Optimization Android SDK in an Android Views app](/personalization/optimization-sdk/integrate-the-optimization-android-sdk-in-a-views-app/) -
  XML Views setup flow for `OptimizationManager`, `OptimizedEntryView`, screen tracking, and preview
  panel mounting.
* [Android reference implementation](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/implementations/android-sdk/README.md) - Native Android
  validation app with Compose and XML Views shells.