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

Готовый UI рендерит SDK; приложение передаёт контекст, размещает inline-блоки и обрабатывает навигацию. Для собственного UI используйте [headless](./flutter_headless_guide.md). Настройка кампании в кабинете: [Кампании в приложении](/personalization/Campaigns/app_campaigns/getting_started.md).

## In-app

`trackView()` и `triggerEvent()` отправляют запрос активации, затем SDK получает контент подходящей кампании. Кандидаты проверяются по убыванию приоритета до первого с доступным корневым контентом. SDK учитывает задержку кампании, актуальность ответа, состояние контекста и presentation lock.

Поддерживаются модальное окно, bottom sheet, полноэкранный формат и SnackBar. Tooltip загружается через якорь. Контент с методом доставки JSON SDK сам не рисует.

Чтобы временно не показывать in-app, используйте [presentation lock](./configuration.md#блокировка-автопоказа). Это не останавливает запросы контента.

## Inline-блок

```dart
GravityInlineWidget(
  selector: 'product_recommendations',
  height: 280,
  pageContext: const PageContext(
    type: ContextType.product,
    data: ['sku-123'],
    location: 'app://product/sku-123',
  ),
  showLoading: true,
  loadingWidget: const Center(child: CircularProgressIndicator()),
)
```

Импорты для виджетов: `package:flutter/material.dart` и `package:gravity_sdk/gravity_sdk.dart`.

| Параметр | Назначение |
|---|---|
| `selector` | Обязательное имя размещения из кампании |
| `pageContext` | Обязательный контекст выбора |
| `placeholderId` | Выбор части контента по placeholder |
| `height`, `width` | Ограничения размеров; nullable, в прокручиваемом экране задавайте подходящую высоту |
| `showLoading`, `loadingWidget` | По умолчанию загрузка видима со стандартным индикатором |
| `backgroundColor` | Фон |
| `onLoaded` | Сообщить приложению об успешной загрузке контента |
| `rules` | Правила рекомендаций |

Без `placeholderId` выбирается корневой контент без placeholder. С `placeholderId` — корневой контент с точно совпавшим значением. Контент шагов не выбирается как корневой.

При ошибке, отсутствии подходящего контента или пустых товарах блок скрывается. `onLoaded` сообщает о загрузке, а не о фактической видимости и не заменяет impression.

Запрос выполняется при создании состояния виджета. При смене selector, товара, контекста или пользователя пересоздавайте блок с новым `Key`, например `ValueKey('user-42:sku-123')`; одна лишь замена параметров существующего состояния не гарантирует новую загрузку.

## Группа inline-блоков

```dart
GravityInlineListWidget(
  group: 'homepage_group',
  height: 250,
  pageContext: const PageContext(
    type: ContextType.homepage,
    data: [],
    location: 'app://homepage',
  ),
  showIndicator: true,
)
```

Виджет получает кампании группы, выбирает корневые контенты и располагает их в горизонтальной карусели по `variables.index`. Доступны настройки загрузки и цвета индикаторов. Параметров `rules`, `placeholderId` и `onLoaded` у этого виджета нет.

## Tooltip

`GravityAnchor` оборачивает элемент, относительно которого будет показана подсказка. Вызовите переданный `onReady`, когда якорь готов. Пример: сначала загрузить inline-контент, затем подсказку:

```dart
GravityAnchor(
  selector: 'profile_tooltip',
  pageContext: const PageContext(
    type: ContextType.other,
    data: [],
    location: 'app://profile',
  ),
  builder: (context, onReady) => GravityInlineWidget(
    selector: 'profile_banner',
    height: 120,
    pageContext: const PageContext(
      type: ContextType.other,
      data: [],
      location: 'app://profile',
    ),
    onLoaded: onReady,
  ),
)
```

Для обычного собственного виджета сообщите о готовности после его layout через `addPostFrameCallback`. Не вызывайте `onReady` как побочный эффект каждого `build()`.

Запросить подсказку вручную можно через `fetchAnchorContent(context: ..., selector: ..., pageContext: ...)`. Якорь с тем же selector должен существовать. Не запускайте одновременно ручную загрузку и автоматическую загрузку того же якоря. Presentation lock на этот путь не действует.

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

Передайте `ProductWidgetBuilder` при инициализации. `Slot.item` — карта атрибутов из фида; названия полей и значения зависят от проекта. В примере используются `name`, `price` и `imageUrl`:

```dart
class ShopProductBuilder extends ProductWidgetBuilder {
  const ShopProductBuilder({required this.onOpenProduct});
  final void Function(Slot product) onOpenProduct;

  @override
  Widget build({
    required BuildContext context,
    required Slot product,
    required CampaignContent content,
    required Campaign campaign,
  }) {
    final item = product.item;
    final imageUrl = item['imageUrl'];
    return InkWell(
      onTap: () {
        GravitySDK.instance.sendProductEngagement(
          ProductClickEngagement(product, content, campaign),
        );
        onOpenProduct(product);
      },
      child: SizedBox(
        width: 160,
        child: Column(
          children: [
            if (imageUrl is String)
              Image.network(imageUrl, height: 100, fit: BoxFit.cover),
            Text(item['name']?.toString() ?? ''),
            Text(item['price']?.toString() ?? ''),
          ],
        ),
      ),
    );
  }
}
```

`onOpenProduct` подключите к реальной навигации вашего каталога при создании билдера. Если билдер не передан, SDK использует стандартную карточку.

В контейнере товаров SDK уже отслеживает видимость карточек, включая созданные вашим билдером: ≥50% один раз для слота. Здесь вручную отправляйте **клик**, а не дополнительный product visible impression. Если вы самостоятельно рисуете весь список товаров вне контейнера SDK, ответственность за оба события лежит на приложении.

## Правила рекомендаций

Пример запроса с фильтром; поле и оператор должны поддерживаться вашим проектом:

```dart
final rules = [
  RtRule(
    type: 'filter',
    conditions: [
      RtRuleCondition(
        field: 'category',
        arguments: [
          RtRuleArgument(action: 'in', value: ['shoes']),
        ],
      ),
    ],
  ),
];

final response = await GravitySDK.instance.getContentBySelector(
  selector: 'category_recommendations',
  pageContext: const PageContext(
    type: ContextType.category,
    data: ['shoes'],
    location: 'app://category/shoes',
  ),
  rules: rules,
);
```

`rules` поддерживаются в `GravityInlineWidget` и методах `getContentBySelector`/`CampaignId`/`Group`, а также `SelectorWithDetails`/`CampaignIdWithDetails`. У `RtRule` есть необязательные `id` и `slots`; у условия — `field` и `arguments`; у аргумента — `action` и список строк `value`. Универсального набора операторов SDK не задаёт.

## Многошаговые кампании и формы

Встроенный рендерер поддерживает переходы по шагам, выбор вариантов, текстовые поля, ограничения ввода и условную видимость элементов. Корневой контент не имеет `step`; дополнительные контенты — имеют. Переход `open_step` обрабатывает SDK. Tooltip в качестве дополнительного шага не поддерживается.

In-app формы настраиваются в кампании; отдельный вызов отправки ответа в приложении не нужен. SDK проверяет обязательные видимые поля и длину ввода, сериализует ответы в строковые свойства `CustomEvent` и отправляет результат, включая `campaignId` и `experienceId`. При временной ошибке результат может остаться в офлайн-очереди.

После отправки переход по URL/диплинку всё равно требует вашего callback. Закрытие формы или переход UI не означает, что результат уже доставлен серверу. Не дублируйте событие отправки формы в callback приложения.

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

| UI | Автоматически | Вручную |
|---|---|---|
| Готовые in-app/inline-компоненты | Tracking загрузки, показов и настроенных действий | Навигация приложения; не дублировать tracking |
| Карточка через `ProductWidgetBuilder` в контейнере SDK | Загрузка контента и видимость товаров | Клик по товару |
| Полностью собственный UI | Tracking загрузки при `getContentBy*` | Фактический показ, visible impression, клик; для товаров — product engagement |

Отправка использует tracking-события из ответа сервера. Если нужных `events`/обработчиков нет, SDK не может построить событие самостоятельно. Подробности — [Аналитика собственного UI](./flutter_headless_guide.md#аналитика-собственного-ui).
