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

# Guides

> Patterns and recipes for common Kotlin SDK integration tasks.

## Handling no-fill

Subscribe to `AdEvent.NoFill` to know when no ad was returned (geo-restriction, frequency cap, etc.). The `skipCode` payload tells you why.

```kotlin theme={null}
onEvent = { event ->
    if (event is AdEvent.NoFill) {
        android.util.Log.d("kontext", "no fill: ${event.skipCode}")
    }
}
```

## Lifecycle management

Always call `session.destroy()` (or its alias `session.close()`) when the conversation ends or the screen is closed. It cancels in-flight preloads, tears down every mounted ad, finalizes the OMID session, and releases WebView resources. Idempotent.

In Compose, anchor it to the screen-level `DisposableEffect`:

```kotlin theme={null}
DisposableEffect(session) {
    onDispose { session.close() }
}
```

In a ViewModel:

```kotlin theme={null}
override fun onCleared() {
    session.destroy()
    super.onCleared()
}
```

## Live-updating session options

A subset of session options can be live-updated without recreating the session. Updates are read on the next `/preload`, so changes take effect on the next user message.

```kotlin theme={null}
import so.kontext.ads.model.MutablePublisherOptions

session.updateOptions(MutablePublisherOptions(variantId = "new-variant"))
```

Live-updateable fields: `variantId`, `regulatory`, `userEmail`, `advertisingId`. Non-null fields overwrite; null fields are left unchanged. To clear a field, recreate the session.

<Warning>
  `publisherToken`, `userId`, `conversationId`, `enabledPlacementCodes`, and `character` are **not** live-updateable. Changing them mid-session would desync the `/init` registration or leave the accumulated message history targeted at the wrong persona. Recreate the session instead.
</Warning>

## Live-updating consent mid-session

When the user updates their consent in your CMP, call:

```kotlin theme={null}
import so.kontext.ads.model.Regulatory

session.updateOptions(MutablePublisherOptions(
    regulatory = Regulatory(gdpr = 1, gdprConsent = "<new-TCF-string>"),
))
```

The next preload picks up the new value — no session recreation needed.

## Switching character

The active `character` cannot be live-updated — the accumulated message history belongs to the original persona, so swapping mid-session would leave messages targeted at the wrong character.

To switch character, destroy the current session and create a new one:

```kotlin theme={null}
session.destroy()

session = KontextAds.createSession(
    context = applicationContext,
    options = SessionOptions(
        publisherToken = "<your-publisher-token>",
        userId = "user-1234",
        conversationId = "conv-new",
        character = newCharacter,
    ),
)
```

The same applies to `publisherToken`, `userId`, `conversationId`, and `enabledPlacementCodes` — recreate the session whenever any of those change.

## Loading older messages (conversation restore)

When restoring a conversation from your backend, call `addMessage(...)` once per historical message in order. Preloads are debounced, so rapid sequential calls coalesce into a single preload for the most recent user message — you won't fire one preload per restored message.

```kotlin theme={null}
for (historical in loadedFromBackend) {
    session.addMessage(
        Message(
            id = historical.id,
            role = historical.role,
            content = historical.content,
        ),
    )
}
```

## Pass every message, suppress ads with `trackOnly`

Always feed every message into the session, even when you don't want an ad to appear (e.g. when a user is on a paid tier, in a no-ads region, or you've decided to show ads only every Nth message). Skipping `addMessage(...)` calls breaks the conversation context the server relies on for targeting. See [Pacing](/concepts/pacing) for the full pattern.

```kotlin theme={null}
import so.kontext.ads.model.AddMessageOptions

val shouldShowAd = userMessageCount % 5 == 0

session.addMessage(
    Message(id = msg.id, role = Role.USER, content = msg.content),
    AddMessageOptions(trackOnly = !shouldShowAd),
)
```

When `trackOnly = true`, the preload still fires (server keeps full analytics) but no `AdEvent.Filled` will arrive for that message and `session.createAd(...)` won't resolve to a fillable ad.

## Theming

Pass a publisher-defined theme string to the `InlineAd` composable; it propagates into the iframe's `update-iframe` payload and is available to ad creatives.

```kotlin theme={null}
InlineAd(
    messageId = msg.id,
    session = session,
    theme = if (isSystemInDarkTheme()) "dark" else "light",
)
```

Theme is part of the `Ad` identity key — flipping it for the same `messageId` causes the iframe to reload with the new value.

## View interop (non-Compose)

For apps not on Jetpack Compose, use `InlineAdView` (a `FrameLayout` subclass). Inflate or instantiate it like any other view, then call `bind(messageId, session)`:

```kotlin theme={null}
import so.kontext.ads.ui.InlineAdView

val adView = InlineAdView(context)
adView.layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
adView.onHeightChange = { newHeight ->
    // Resize the surrounding row if you're inside a RecyclerView
    requestRowLayout()
}
adView.bind(messageId = assistantMessageId, session = session)
container.addView(adView)
```

The view follows the same lifecycle rules as the composable — call `session.destroy()` on screen teardown; the view's `WebView` is recycled by the session's pool.
