> 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/_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` 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("headline")) resolvedEntry.contentTypeId == "cta" && resolvedEntry.hasField("label") -> renderCta(resolvedEntry.getField("label")) else -> renderPage(resolvedEntry.getField("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` 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. > 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.