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

Готовый UI рендерит SDK; приложение передаёт контекст, размещает inline-блоки и обрабатывает навигацию. Для собственного UI используйте headless. Настройка кампании в кабинете: Кампании в приложении.

In-app

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

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

Чтобы временно не показывать in-app, используйте presentation lock. Это не останавливает запросы контента.

Inline-блок

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-блоков

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-контент, затем подсказку:

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:

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, ответственность за оба события лежит на приложении.

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

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

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.