# Кампании и рекомендации в Android

Сначала [подключите SDK](./overview.md) и опубликуйте [кампанию в приложении](/personalization/Campaigns/app_campaigns/getting_started.md). Навигация по URL/deeplink реализуется в приложении через [callback](./configuration.md).

## In-App кампании

SDK автоматически отображает in-app-кампании, если они приходят в ответ на `trackView(...)` или `triggerEvent(...)`.

Автопоказ поддерживает `MODAL`, `BOTTOM_SHEET`, `FULL_SCREEN` и `SNACK_BAR`. Ответ должен содержать непустой `variables.elements`; `INLINE` в этот flow не выводится. SDK проверяет, что Activity не уничтожена и не завершается, и учитывает presentation lock.

Приложение отвечает за:

- корректный `PageContext`;
- корректный `activityContext`;
- обработку действий пользователя через `gravityEventCallback`.
Блокировка новых in-app показов описана в [Конфигурации](./configuration.md#блокировка-показа-in-app-кампаний).

## `GravityInlineView`

Используйте `GravityInlineView`, если экран построен на XML.

```xml
<ai.gravityfield.gravity_sdk.ui.GravityInlineView
    android:id="@+id/recommendationsView"
    android:layout_width="match_parent"
    android:layout_height="250dp"
    app:selector="&quot;homepage-recs&quot;"
    app:color="#FFF1F1F1"
    app:cornerRadius="20dp" />
```

После инфлейта обязательно передайте `PageContext`:

```kotlin
val inlineView = findViewById<GravityInlineView>(R.id.recommendationsView)
inlineView.init(
    PageContext(
        type = ContextType.HOMEPAGE,
        data = emptyList(),
        location = "app://homepage",
    )
)
```

Поддерживаемые XML-атрибуты:

| Атрибут | Описание |
| :--- | :--- |
| `app:selector` | Селектор кампании. |
| `app:color` | Цвет фона контейнера. |
| `app:cornerRadius` | Единый радиус скругления. |
| `app:cornerRadiusTopStart` | Радиус верхнего левого угла. |
| `app:cornerRadiusTopEnd` | Радиус верхнего правого угла. |
| `app:cornerRadiusBottomStart` | Радиус нижнего левого угла. |
| `app:cornerRadiusBottomEnd` | Радиус нижнего правого угла. |
| `app:loaderLayout` | Кастомный layout для состояния загрузки. |

## `GravityInlineCompose`

Используйте `GravityInlineCompose`, если экран написан на Jetpack Compose.

```kotlin
GravityInlineCompose(
    modifier = Modifier
        .fillMaxWidth()
        .height(250.dp),
    selector = JSONObject.quote("homepage-recs"),
    pageContext = PageContext(
        type = ContextType.HOMEPAGE,
        data = emptyList(),
        location = "app://homepage",
    ),
    loader = { CircularProgressIndicator() },
)
```

`loader` - это composable для состояния загрузки. Если placeholder не нужен, можно передать `loader = null`.

## `GravityInlineListView`

`GravityInlineListView` используется для отображения нескольких inline-элементов из одной группы.

```xml
<ai.gravityfield.gravity_sdk.ui.GravityInlineListView
    android:id="@+id/inlineListView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:groupSelector="&quot;homepage-group&quot;" />
```

```kotlin
val inlineListView = findViewById<GravityInlineListView>(R.id.inlineListView)
inlineListView.init(
    PageContext(
        type = ContextType.HOMEPAGE,
        data = emptyList(),
        location = "app://homepage",
    )
)
```

Этот компонент полезен для сценариев, где несколько блоков должны быть загружены одной группой кампаний.

## Кэш inline-блоков

`GravityInlineView`, `GravityInlineCompose` и `GravityInlineListView` используют кэш, чтобы восстановить контент и позицию скролла при пересоздании view. Это важно для экранов с `RecyclerView`, повторным использованием view и несколькими inline-блоками на одном экране.

Кэш привязан к полному ключу:

- для `GravityInlineView` и `GravityInlineCompose` - `selector + PageContext`;
- для `GravityInlineListView` - `groupSelector + PageContext`.

SDK не очищает inline-кэш автоматически, потому что не знает, закрыт экран окончательно или view будет переиспользована. При закрытии экрана сбросьте кэш самостоятельно:

```kotlin
class InlineDemoActivity : AppCompatActivity() {
    private val pageContext = PageContext(
        type = ContextType.HOMEPAGE,
        data = emptyList(),
        location = "app://homepage",
    )

    override fun onDestroy() {
        if (isFinishing) {
            GravitySDK.instance.resetInlineViewCache(
                selector = JSONObject.quote("homepage-recs"),
                pageContext = pageContext,
            )
        }
        super.onDestroy()
    }
}
```

Для `GravityInlineListView` используйте отдельный метод:

```kotlin
GravitySDK.instance.resetInlineListViewCache(
    groupSelector = JSONObject.quote("homepage-group"),
    pageContext = pageContext,
)
```

Если на экране несколько inline-блоков, сбрасывайте кэш для каждого `selector` или `groupSelector`, который использовался на этом экране. Для Fragment/Compose выбирайте момент окончательного выхода из маршрута; уничтожение view при смене конфигурации не всегда означает закрытие экрана. Сброс удаляет данные кэша, но не запускает повторную загрузку уже отображённого view.

Используйте тот же экземпляр значения selector/groupSelector, включая JSON-кодирование, и тот же `PageContext`, что при загрузке. Отличие любого поля контекста создаёт другой ключ.

## Карточки товаров

`ProductViewBuilder` нужен для отображения карточек товара в блоке рекомендаций. Для карточек в дизайне приложения передайте SDK свой шаблон. Если builder отсутствует, Android использует демонстрационную GravityProduct; не рассчитывайте на неё как на карточку вашего приложения.

SDK получает recommendation-блок, управляет контейнером, списком слотов и tracking-контекстом, но сам шаблон товарной карточки задает приложение. Разработчик реализует `ProductViewBuilder` или `LegacyProductViewBuilder` и передает его в `GravitySDK.initialize(...)` через параметр `productViewBuilder`.

После этого SDK использует переданный шаблон для каждого товара в product/recommendation-блоках: вызывает builder для каждого `slot` и передает в него `slot`, `content` и `campaign`. Поэтому карточка отображается как native UI приложения, а не как готовая карточка, нарисованная SDK.

`slot` содержит данные одного рекомендованного товара:

- `slot.item` - словарь с полями товара из фида или из запрошенных `fields`: например `sku`, `name`, `price`, `old_price`, `image_url`, `url`, `brand`, `currency` и другие кастомные поля;
- `slot.strId` и `slot.slotId` - идентификаторы позиции товара в выдаче;
- `slot.fallback` - признак, что товар пришел из fallback-логики;
- `slot.events` - tracking-данные товара, которые SDK использует при отправке engagement.

Названия ключей в `slot.item` должны совпадать с вашим фидом и настройкой `fields`. Например, если в ответе приходит `imageUrl`, читайте `imageUrl`; если используется поле фида `image_url`, читайте `image_url`.

Важно:

- SDK автоматически отслеживает visible impression товара во встроенном блоке;
- клик по товару в кастомной карточке нужно отправлять вручную через `sendProductEngagement(...)`.

Пример шаблона карточки для Jetpack Compose:

Полный пример: [product_view_builder.kt](./examples/product_view_builder.kt). Он использует Coil 3.1.0; [зависимости для загрузки изображений](./bdui.md#подключите-полный-пример). Замените package на package приложения, реализуйте openProduct и передайте MyComposeProductViewBuilder в initialize.

## Кто отправляет engagement

Успешная загрузка автоматически отправляет content load tracking. Встроенные UI-компоненты отправляют показы контента, видимость товаров и tracking для поддерживаемых действий. Карточка из ProductViewBuilder уже обёрнута detector SDK: вручную отправляйте её клик перед навигацией, не дублируйте visible impression.

Ручные вызовы engagement не генерируют tracking callback. Для собственного UI используйте [отдельные правила аналитики](./custom_ui.md#аналитика-собственного-ui).

Android ждёт видимость не менее 50% в течение секунды; после выхода и возвращения событие может повториться.
