Contentful Personalization & Analytics
    Preparing search index...

    Integrating the Optimization Android SDK in an XML Views app

    Use this guide when you want to add Personalization, Analytics, screen tracking, and preview overrides to a native Android application built with XML layouts or Android Views.

    For shared runtime behavior, consent gates, tracking thresholds, live-update precedence, and offline delivery, see Android SDK runtime and interaction mechanics. Use the Compose guide instead if your app is Compose-based: Integrating the Optimization Android SDK in a Jetpack Compose app.

    Table of Contents

    The XML Views integration uses the SDK's View-based adapter surface:

    • OptimizationManager initializes one process-wide OptimizationClient and stores global tracking and live-update defaults.
    • OptimizedEntryView resolves a personalized Contentful entry and can attach view and tap tracking.
    • TrackingRecyclerView nudges descendant OptimizedEntryView instances to re-check visibility while lists scroll.
    • ScreenTracker emits screen events from Activity or Fragment lifecycle methods.
    • OptimizationManager.attachPreviewPanel(...) mounts the developer-only preview panel entry point.

    The SDK does not replace your Contentful delivery client. Your application still owns Contentful fetching, consent UX, identity policy, navigation, and rendering.

    Most XML Views integrations follow this sequence:

    1. Add the Maven dependency and create an OptimizationConfig.
    2. Initialize OptimizationManager from Application.onCreate.
    3. Apply the application's consent policy: seed consent when default-on SDK activity is permitted, or collect consent in app UI.
    4. Read OptimizationManager.client from activities or fragments that render Contentful content.
    5. Fetch Contentful entries with linked optimization references.
    6. Render each Contentful entry through OptimizedEntryView.
    7. Enable view and tap tracking where they fit the screen.
    8. Emit screen events from Activity or Fragment lifecycle methods.

    Optional additions include live updates when entries need to react to optimization state changes after initial render, and the preview panel when authors or engineers need local audience and variant overrides.

    The Android reference implementation in this repository demonstrates the same SDK behavior in Compose and XML Views shells:

    Add the Android SDK from Maven Central as described in the Optimization Android SDK README. In an Android application module, the dependency looks like this:

    dependencies {
    implementation("com.contentful.java:optimization-android:<version>")
    }

    Then create an OptimizationConfig with the Optimization client ID and the Contentful locale information your app uses when fetching entries:

    val optimizationConfig = OptimizationConfig(
    clientId = "your-client-id",
    environment = "master",
    contentfulLocales = ContentfulLocales(default = "en-US"),
    locale = "en-US",
    debug = BuildConfig.DEBUG,
    )

    Only clientId is required. If application policy permits Optimization by default and no end-user consent UI is rendered, set defaults = StorageDefaults(consent = true). Otherwise, leave defaults unset and connect client.consent(true) and client.consent(false) to the app's consent UI.

    Use contentfulLocales and locale when the same screen renders localized Contentful entries. For the full locale model, see Locale handling in the Optimization SDK Suite.

    Initialize the SDK once from Application.onCreate. OptimizationManager owns the process-wide client used by View-based activities and fragments.

    class MyApplication : Application() {
    override fun onCreate() {
    super.onCreate()

    OptimizationManager.initialize(
    context = this,
    config = optimizationConfig,
    trackViews = true,
    trackTaps = false,
    liveUpdates = false,
    previewPanel = PreviewPanelConfig(
    contentfulClient = previewContentfulClient,
    ),
    )
    }
    }

    Read the client from any Activity or Fragment after initialization:

    class HomeActivity : AppCompatActivity() {
    private val client: OptimizationClient
    get() = OptimizationManager.client
    }

    OptimizationManager.initialize(...) starts SDK initialization asynchronously. Observe client.isInitialized before running work that depends on the bridge being ready. For lifecycle details, see Android SDK runtime and interaction mechanics.

    If your application policy permits Optimization by default, seed accepted consent in OptimizationConfig and omit consent controls:

    val optimizationConfig = OptimizationConfig(
    clientId = "your-client-id",
    defaults = StorageDefaults(consent = true),
    )

    That starts all gated SDK events immediately and permits durable profile-continuity storage for profile, selected optimizations, changes, and the anonymous ID.

    When application policy depends on user choice, leave consent unset and call client.consent(true | false) from an application-owned consent UI.

    class ConsentActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    acceptButton.setOnClickListener {
    OptimizationManager.client.consent(true)
    }

    rejectButton.setOnClickListener {
    OptimizationManager.client.consent(false)
    }

    lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
    OptimizationManager.client.state.collect { state ->
    consentBanner.isVisible = state.consent == null
    }
    }
    }
    }
    }

    identify and screen remain allowed before consent so a mobile journey can establish profile context and anonymous screen analytics. For cross-SDK consent policy guidance, see Consent management in the Optimization SDK Suite.

    The consent value is persisted and restored on later launches. Profile-continuity state persists only when persistence consent allows it. Use the app's consent policy to decide whether a stored value remains valid.

    Use OptimizationManager.client.consent(events = true, persistence = false) when events are allowed but durable profile continuity must stay session-only.

    OptimizedEntryView is the View-based component for rendering Contentful entries through the Optimization resolver. It passes non-personalized entries through unchanged, resolves personalized entries against the selected variants for the visitor, and can attach view and tap tracking.

    Fetch entries from Contentful as single-locale JSON-shaped maps and include linked optimization references in the payload. Pass those maps to OptimizedEntryView.

    Use the resolved client.locale value for app-owned Contentful Delivery API requests that feed SDK entry resolution:

    lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
    OptimizationManager.client.isInitialized.collect { isInitialized ->
    if (isInitialized) {
    val locale = OptimizationManager.client.locale ?: "en-US"
    val entries = contentfulClient.fetchHomeEntries(locale = locale)
    renderEntries(entries)
    }
    }
    }
    }

    The resolver expects the same single-locale CDA entry contract used by the other SDK runtimes. For details, see Entry personalization and variant resolution.

    fun createHeroView(entry: Map<String, Any>): View {
    return OptimizedEntryView(this).apply {
    trackTaps = true
    accessibilityIdentifier = "home-hero-personalization"
    setContentRenderer { resolvedEntry ->
    HeroBinder.create(context, resolvedEntry)
    }
    setEntry(entry)
    }
    }

    The renderer receives the resolved entry map. The application owns converting fields from that map into the view model or View hierarchy it wants to render.

    OptimizedEntryView checks visibility from its own layout callbacks. Use TrackingRecyclerView when a scrolling list needs an additional signal on every scroll frame.

    val recyclerView = TrackingRecyclerView(this).apply {
    layoutManager = LinearLayoutManager(this@HomeActivity)
    adapter = ContentEntryAdapter(entries)
    }

    In each item view, wrap the rendered Contentful entry with OptimizedEntryView.

    5. Track entry interactions

    OptimizationManager.initialize(...) defines defaults for every OptimizedEntryView:

    OptimizationManager.initialize(
    context = this,
    config = optimizationConfig,
    trackViews = true,
    trackTaps = false,
    )

    View tracking defaults to on. Tap tracking defaults to off because taps are usually tied to application-specific navigation or business actions.

    Override tracking per entry

    OptimizedEntryView(context).apply {
    trackViews = false
    setContentRenderer { resolvedEntry -> HeroBinder.create(context, resolvedEntry) }
    setEntry(hero)
    }

    OptimizedEntryView(context).apply {
    trackTaps = true
    setContentRenderer { resolvedEntry -> CtaBinder.create(context, resolvedEntry) }
    setEntry(cta)
    }

    OptimizedEntryView(context).apply {
    onTap = { resolvedEntry -> navigateToEntry(resolvedEntry) }
    setContentRenderer { resolvedEntry -> CtaBinder.create(context, resolvedEntry) }
    setEntry(cta)
    }

    Setting trackTaps = false disables tap tracking even when onTap is present. For timing thresholds and event delivery behavior, see Android SDK runtime and interaction mechanics.

    Call ScreenTracker.trackScreen(...) from Activity.onResume or Fragment.onResume:

    override fun onResume() {
    super.onResume()
    ScreenTracker.trackScreen("Home")
    }

    For dynamic names or tracking after data loads, call the client directly:

    lifecycleScope.launch {
    OptimizationManager.client.screen(
    name = "BlogPostDetail",
    properties = mapOf("postId" to postId),
    )
    }

    By default, OptimizedEntryView locks to the first variant it resolves so content does not change while a visitor is reading it. Enable live updates when a screen needs to react to profile or preview changes without a reload:

    OptimizationManager.initialize(
    context = this,
    config = optimizationConfig,
    liveUpdates = true,
    )

    OptimizedEntryView(context).apply {
    liveUpdates = true
    setContentRenderer { resolvedEntry -> DashboardBinder.create(context, resolvedEntry) }
    setEntry(dashboard)
    }

    The preview panel forces live updates while it is open. For precedence rules, see Android SDK runtime and interaction mechanics.

    Gate the preview panel behind a debug or internal-build flag. In XML Views apps, call OptimizationManager.attachPreviewPanel(...) from each Activity that displays the floating entry point.

    class HomeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.home)

    if (BuildConfig.DEBUG) {
    OptimizationManager.attachPreviewPanel(this)
    }
    }
    }

    Pass PreviewPanelConfig(contentfulClient = previewContentfulClient) to OptimizationManager.initialize(...) when the panel needs to display audience and experience names. Without a PreviewContentfulClient, the panel displays identifiers.

    This example combines application-level initialization, preview-panel gating, screen tracking, entry rendering, and tap tracking:

    class MyApplication : Application() {
    override fun onCreate() {
    super.onCreate()

    OptimizationManager.initialize(
    context = this,
    config = optimizationConfig,
    trackViews = true,
    trackTaps = false,
    previewPanel = PreviewPanelConfig(
    contentfulClient = previewContentfulClient,
    ),
    )
    }
    }

    class HomeActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val list = TrackingRecyclerView(this).apply {
    layoutManager = LinearLayoutManager(this@HomeActivity)
    }
    setContentView(list)

    if (BuildConfig.DEBUG) {
    OptimizationManager.attachPreviewPanel(this)
    }

    lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
    OptimizationManager.client.isInitialized.collect { isInitialized ->
    if (isInitialized) {
    val locale = OptimizationManager.client.locale ?: "en-US"
    val entries = fetchHomeEntries(locale = locale)
    list.adapter = ContentEntryAdapter(entries)
    }
    }
    }
    }
    }

    override fun onResume() {
    super.onResume()
    ScreenTracker.trackScreen("Home")
    }
    }

    fun bindEntry(parent: ViewGroup, entry: Map<String, Any>) {
    parent.addView(
    OptimizedEntryView(parent.context).apply {
    trackTaps = true
    setContentRenderer { resolvedEntry ->
    ContentEntryBinder.create(context, resolvedEntry)
    }
    setEntry(entry)
    },
    )
    }
    • Android reference implementation - Demonstrates Compose and XML Views shells that exercise native Android bridge behavior, entry resolution, interaction tracking, screen tracking, live updates, and preview-panel overrides against the same mock API.