# Свой UI и Custom JSON в Android

Используйте этот путь, когда приложение само рисует кампанию или рекомендации. SDK получает контент и отправляет content load tracking; видимость, клики и навигацию определяет приложение. Сначала выполните [инициализацию](./configuration.md).

## `getContentBySelector(...)`

Для `getContentBySelector(...)` требуется корректно заполненный [`PageContext`](./events.md#pagecontext).

Пример:

```kotlin
lifecycleScope.launch {
    val response = GravitySDK.instance.getContentBySelector(
        selector = JSONObject.quote("homepage-banner-json"),
        pageContext = PageContext(
            type = ContextType.HOMEPAGE,
            data = emptyList(),
            location = "app://home",
        )
    )

    if (response != null && response.data.isNotEmpty()) {
        val campaign = response.data.first()
        val variation = campaign.payload.firstOrNull()
        val content = variation?.contents?.firstOrNull()
        // Отрисуйте content в своем UI
    }
}
```

Метод возвращает `null` при сетевой ошибке или ошибке разбора ответа; успешный ответ без подходящей кампании содержит пустой `data`. Успешная загрузка автоматически отправляет content load tracking для всех `contents`, но не показывает UI.

Для выбора по ID и группе доступны `getContentByCampaignId(...)` и `getContentByGroupSelector(...)`: [сигнатуры](./api_reference.md#загрузка-контента).

Все три метода используют общие `Options`/`ContentSettings` и отправляют content load tracking.

## Формат selector в Android 1.0.4

В опубликованном AAR и его исходниках selector, group и campaign ID обрабатываются через `Json.parseToJsonElement(...)`. Поэтому строковое значение нужно передавать как JSON-строку. Для произвольного selector используйте `org.json.JSONObject.quote(...)`:

```kotlin
val sdkSelector = JSONObject.quote("homepage-recommendations")
val response = GravitySDK.instance.getContentBySelector(
    selector = sdkSelector,
    pageContext = pageContext,
)
```

Та же особенность действует для `GravityInlineCompose`, `GravityInlineView`, `GravityInlineListView` и строкового `campaignId`. В XML JSON-кавычки задаются как `&quot;`. Это обход текущей реализации: имя selector в кабинете остаётся обычной строкой без добавленных кавычек. Не кодируйте уже подготовленное значение повторно.

SDK также сериализует `ContentSettings` под ключом `option` внутри `data`, в то время как iOS использует `options`. Если интеграция зависит от `fields` или `skusOnly`, проверьте фактический запрос и результат на вашем backend; документация не гарантирует, что backend применит эти настройки в Android 1.0.4.

## Какие параметры нужны для получения контента

Для вызова `getContentBySelector(...)` обязательны:

- `selector`;
- [`pageContext.type`](./events.md#pagecontext);
- [`pageContext.data`](./events.md#pagecontext);
- [`pageContext.location`](./events.md#pagecontext).

Опциональны:

- [`lng`](./events.md#pagecontext);
- `pageNumber`;
- `referrer`;
- `utm`;
- `attributes`.

`skusOnly` и `fields` не передаются в метод напрямую. Они задаются один раз через `setOptions(...)` в `ContentSettings` и затем сериализуются в последующих запросах `choose`. Применение этих полей сервером в 1.0.4 проверьте с учётом ключа `option`, описанного выше.

## Как устроен ответ `ContentResponse`

Упрощенно структура ответа выглядит так:

```kotlin
ContentResponse
  -> data: List<Campaign>
      -> payload: List<CampaignVariation>
          -> contents: List<CampaignContent>
              -> products?.slots: List<Slot>
```

Это важно для manual rendering и engagement:

- `campaign` берите из `response.data`;
- `content` берите из `campaign.payload[*].contents[*]`;
- `slot` берите из `content.products?.slots[*]`.

`decisionId` присутствует в `CampaignVariation`, но публичные методы `sendContentEngagement(...)` и `sendProductEngagement(...)` не требуют передавать его отдельным параметром.

## Custom JSON

Пользовательский JSON приходит в `content.custom?.json` как строка. Для кампании с JSON `{ "variant": "compact" }` приложение само разбирает конфигурацию:

```kotlin
val campaign = response?.data?.firstOrNull()
val content = campaign?.payload?.firstOrNull()?.contents?.firstOrNull()
val config = content?.custom?.json?.let { raw ->
    runCatching { JSONObject(raw) }.getOrNull()
}
val variant = config?.optString("variant", "control") ?: "control"
// Покажите UI для variant; неизвестное значение обработайте своим fallback.
```

Вариант A/B выбирает сервер. Сохраняйте `campaign`/`content` для engagement; impression отправляйте после фактического показа выбранного варианта, бизнес-события — в момент действия.

Отдельного параметра `headless` у native SDK нет. Методы выбора контента сами не показывают UI, но `trackView(...)` и `triggerEvent(...)` могут запустить in-app кампанию. Для собственного UI используйте selector/Custom JSON кампании. Presentation lock не отменяет HTTP-запросы и content load tracking.

## Аналитика собственного UI

Загрузка не означает показ. Сохраните исходные campaign/content и при работе с товарами slot. Передавайте их в engagement после фактического взаимодействия:

| Действие | Тип engagement |
|---|---|
| Контент появился в интерфейсе | `ContentImpressionEngagement` |
| Контент достиг порога видимости | `ContentVisibleImpressionEngagement` |
| Контент закрыт | `ContentCloseEngagement` |
| Карточка товара достигла порога видимости | `ProductVisibleImpressionEngagement` |
| Пользователь нажал на товар | `ProductClickEngagement` |

Порог и дедупликацию реализует приложение. Встроенный Android detector использует 50% площади в течение секунды; полный пример дополнительно дедуплицирует показ каждого размещения.

Публичного ContentClickEngagement нет. sendContentEngagement/sendProductEngagement вызывают tracking URL, но не gravityEventCallback. Для бизнес-действий отправляйте TriggerEvent отдельно. [Справочник engagement](./api_reference.md#engagement).
