> ## 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.

# 实践指南

> Kotlin SDK 常见集成场景的模式与配方。

## 处理 no-fill

订阅 `AdEvent.NoFill` 即可知道何时没有广告返回（地域限制、频次封顶等）。`skipCode` 字段说明了原因。

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

## 生命周期管理

对话结束或页面关闭时，请始终调用 `session.destroy()`（或其别名 `session.close()`）。它会取消正在进行的预加载、销毁所有挂载的广告、结束 OMID 会话并释放 WebView 资源。幂等。

在 Compose 中，挂在页面级的 `DisposableEffect` 上：

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

在 ViewModel 中：

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

## 动态更新会话选项

会话选项的一部分字段可以在不重建 session 的情况下动态更新。更新在下一次 `/preload` 时被读取，因此在下一条用户消息后生效。

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

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

可动态更新的字段：`variantId`、`regulatory`、`userEmail`、`advertisingId`。非 null 字段覆盖原值；null 字段保持不变。如果要清空某个字段，请重建会话。

<Warning>
  `publisherToken`、`userId`、`conversationId`、`enabledPlacementCodes` 和 `character` **不可** 动态更新。中途修改会导致 `/init` 注册与后续请求脱节，或让累积的消息历史指向错误的角色。这些场景请重建会话。
</Warning>

## 中途更新同意状态

当用户在你的 CMP 中更新了同意状态，调用：

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

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

下一次预加载会带上新值——无需重建 session。

## 切换 character

`character` 不能动态更新——已累积的消息历史属于原本的角色；中途切换会导致后续消息以错误的人物画像被定向。

切换角色请销毁当前会话并新建：

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

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

`publisherToken`、`userId`、`conversationId`、`enabledPlacementCodes` 同理——任何一个变化都请重建会话。

## 载入历史消息（会话恢复）

从你的后端恢复会话时，按顺序对每条历史消息调用一次 `addMessage(...)`。预加载本身带有防抖，因此连续的 add 调用会合并为对最近一条用户消息的单次预加载——不会因为恢复了 N 条消息就发出 N 次预加载。

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

## 把每条消息都喂入，用 `trackOnly` 控制是否展示广告

无论是否打算展示广告（付费用户、免广告地区、或每 N 条消息才展示一次广告等），都请把消息全部喂入会话。跳过 `addMessage(...)` 调用会破坏服务端用于定向的对话上下文。完整模式见 [Pacing](/concepts/pacing)。

```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),
)
```

当 `trackOnly = true` 时，预加载仍会发送（服务端保留完整分析数据），但不会为该条消息触发 `AdEvent.Filled`，`session.createAd(...)` 也不会拿到可填充的广告。

## 主题

向 `InlineAd` composable 传入一个发布方自定义的主题字符串；它会进入 iframe 的 `update-iframe` 负载，供广告创意读取。

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

主题是 `Ad` 身份键的一部分——同一个 `messageId` 下切换主题会让 iframe 以新的主题重新加载。

## View 互操作（非 Compose）

非 Compose 项目请使用 `InlineAdView`（`FrameLayout` 子类）。像普通 view 一样实例化或 inflate，然后调用 `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 ->
    // 如果你在 RecyclerView 行里，请重新触发行布局
    requestRowLayout()
}
adView.bind(messageId = assistantMessageId, session = session)
container.addView(adView)
```

`InlineAdView` 遵循与 composable 相同的生命周期规则——页面销毁时调用 `session.destroy()`；view 内的 `WebView` 由会话的池来回收。
