# Гайд: Headless-режим с Flutter SDK

Этот гайд предназначен для разработчиков, которые хотят использовать возможности персонализации Gravity Field, но полностью контролировать UI и UX в своём Flutter-приложении.

**Headless-режим** означает, что SDK выполняет только логику таргетинга и доставки данных, а отрисовка интерфейса и отправка событий взаимодействия (impressions, clicks) лежит на плечах приложения.

Пользовательская настройка кампаний в кабинете описана в разделе [Кампании в приложении](/personalization/Campaigns/app_campaigns/getting_started.md).

---

## 1. Концепции и поток данных

В Headless-режиме SDK выступает в роли "поставщика данных". Вы делаете запрос с контекстом, SDK возвращает JSON-пейлоад (например, цвет кнопки, текст баннера или список товаров), а ваше приложение рендерит это на экране.

**Критически важно:** Так как SDK не контролирует UI, оно **не может автоматически отследить показ (Impression) или клик (Click)**. Вы **обязаны** вызывать методы трекинга вручную, иначе аналитика кампании будет пустой.

```mermaid
sequenceDiagram
    participant App as App
    participant SDK as SDK
    participant Server as Server

    Note over App: 1. Запрос контента
    App->>SDK: trackViewNoShow / triggerEventNoShow
    SDK->>Server: Request (Context + User ID)
    Server-->>SDK: Response (JSON + DecisionID)
    SDK-->>App: GravityDataResponse

    Note over App: 2. Рендеринг UI
    App->>App: Парсинг JSON и отображение виджетов

    Note over App: 3. Обязательный трекинг
    App->>SDK: sendContentEngagement(Impression)
    SDK->>Server: WRIMP (Показ засчитан)

    opt Пользователь нажал на элемент
        App->>SDK: sendContentEngagement(Click)
        SDK->>Server: CLICK (Клик засчитан)
    end
```

---

## 2. Контракты данных

### 2.1. Запрос: PageContext

Корректный контекст — залог того, что кампания сработает. Параметры type и location должны совпадать с настройками таргетинга в дашборде.

!!!warning Внимание
Если вы передадите ContextType.homepage, а кампания настроена на ContextType.cart, SDK не вернёт данные.
!!!

```dart
PageContext(
  type: ContextType.product, // Тип страницы (обязательно)
  data: ['sku-123'], // ID товара/категории (если применимо)
  location: 'app://product/123', // Уникальный ID экрана
)
```

### 2.2. Ответ: `GravityDataResponse`

Когда SDK возвращает данные, вы получаете объект `GravityDataResponse`. Вот где искать ваши данные:

| Поле | Тип | Описание |
|---|---|---|
| `custom` | `dynamic` | **Ваш JSON-пейлоад.** Именно здесь лежат данные, которые вы задали в редакторе кампании (например, `{ "title": "Hello", "color": "#FF0000" }`). |
| `decisionId` | `String` | **Токен для аналитики.** Критически важен для отправки событий `Impression` и `Click`. Без него кампания будет иметь 0 показов в отчётах. |
| `payload` | `CampaignPayload` | Содержит служебную информацию о типе кампании. |
| `campaignId` | `String` | ID кампании (для отладки). |
| `variationId` | `String` | ID вариации (для отладки). |

---

## 3. GravityContentCallback

`GravityContentCallback` — это тип функции, используемый для получения данных из Headless-методов. Поскольку сетевые запросы асинхронны, SDK использует этот колбэк для передачи `GravityDataResponse` обратно в ваше приложение, когда данные готовы.

**Сигнатура:**

```dart
typedef GravityContentCallback = void Function(GravityDataResponse response);
```

**Как использовать:**

Вы можете определить колбэк как метод в вашем классе или передать его как анонимную функцию.

```dart
// Определение обработчика
void onContentReceived(GravityDataResponse response) {
  if (response.custom != null) {
    // Обновляем состояние UI здесь
    setState(() {
      _bannerData = response.custom;
    });
  }
}

// Передача в SDK
GravitySDK.instance.trackViewNoShow(
  context: context,
  pageContext: pageContext,
  onContent: onContentReceived, // Передаём ссылку на функцию
);
```

### Best Practices

*   **State Management:** `GravityContentCallback` вызывается вне цикла сборки (build cycle), поэтому обновления состояния (например, `setState`, `bloc.add`) должны обрабатываться аккуратно.
*   **Обработка ошибок/пустых данных:** Всегда проверяйте, содержит ли `response.custom` или `response.payload` валидные данные перед попыткой отрисовки.
*   **Безопасность контекста:** Если вы обновляете UI внутри колбэка, убедитесь, что виджет всё ещё смонтирован.

---

## 4. Реализация

### 4.1. Получение данных

Есть три основных метода для получения headless-данных. Выберите тот, который подходит под ваш сценарий.

#### Вариант A: При просмотре экрана (`trackViewNoShow`)
Используйте, если кампания привязана к открытию экрана (например, баннер на главной).

```dart
// Define callback
GravityContentCallback onContent = (response) {
  _handleHeadlessResponse(response);
};

    await GravitySDK.instance.trackViewNoShow(
      context: context,
      pageContext: PageContext(
    type: ContextType.homepage,
        data: [],
    location: 'app://home',
  ),
  onContent: onContent,
);
```

#### Вариант B: При событии (triggerEventNoShow)
Используйте, если кампания триггерится действием (например, добавление в корзину).

```dart
await GravitySDK.instance.triggerEventNoShow(
  context: context,
  events: [AddToCartEvent(...)],
  pageContext: PageContext(...),
  onContent: _handleHeadlessResponse, // Pass method reference
);
```

#### Вариант C: Прямой запрос (getContentBySelectorWithDetails)
Используйте, если нужно загрузить конкретную кампанию по её API-селектору.

```dart
    final response = await GravitySDK.instance.getContentBySelectorWithDetails(
  selector: 'my_custom_banner',
  pageContext: PageContext(...),
);

if (response != null) {
  _handleHeadlessResponse(response);
}
```

### 4.2. Обработка ответа и парсинг JSON

Получив `GravityDataResponse`, извлеките данные и `decisionId`.

```dart
void _handleHeadlessResponse(GravityDataResponse response) {
  // 1. Сохраняем decisionId для аналитики
  final decisionId = response.decisionId;

  // 2. Извлекаем Custom JSON
  // Важно: response.custom может быть Map или String (в зависимости от версии SDK)
  Map<String, dynamic> data;
  if (response.custom is String) {
    data = jsonDecode(response.custom);
  } else {
    data = Map<String, dynamic>.from(response.custom);
  }

  // 3. Передаём в UI (например, через State или BLoC)
  setState(() {
    _bannerData = BannerModel.fromJson(data);
    _currentDecisionId = decisionId;
  });
}
```

### 4.3. Обязательный трекинг (Engagement)

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

#### Отправка Impression (Показ)
Вызывайте, когда виджет реально появился на экране (например, в `initState` или через `VisibilityDetector`).

```dart
if (_currentDecisionId != null) {
  GravitySDK.instance.sendContentEngagement(
    ContentImpressionEngagement.fromDecisionId(_currentDecisionId!)
  );
}
```

#### Отправка Click (Клик)
Вызывайте в обработчике нажатия (например, `onTap`).

```dart
GestureDetector(
  onTap: () {
    if (_currentDecisionId != null) {
      GravitySDK.instance.sendContentEngagement(
        ContentClickEngagement.fromDecisionId(_currentDecisionId!)
      );
    }
    // ... ваша логика перехода
  },
  child: ...
)
```

---

## 5. Best Practices & Architecture

### 5.1. Разделение ответственности
Не смешивайте логику SDK с UI-кодом. Используйте промежуточный слой (Repository или Service), который:
1. Вызывает SDK.
2. Парсит `GravityDataResponse` в доменную модель.
3. Возвращает пару `(Model, DecisionId)`.

### 5.2. State Management
Headless-запросы асинхронны. Используйте BLoC, Riverpod или Provider для управления состояниями:
- `Loading`: пока ждём ответ от SDK.
- `Content`: данные пришли, отображаем UI.
- `Empty/Error`: кампания не найдена или ошибка (показываем fallback или ничего).

---

## 6. Типичные ошибки (Troubleshooting)

| Проблема | Причина | Решение |
|---|---|---|
| **Ghost Impressions** | Кампания работает, но в отчётах 0 показов. | Вы забыли вызвать `sendContentEngagement(Impression)`. |
| **Missing Engagement** | Высокие показы, но 0 кликов (CTR 0%). | Не настроен вызов `sendContentEngagement(Click)` на `onTap`. |
| **Context Mismatch** | Кампания не возвращается. | `PageContext` в коде не совпадает с настройками таргетинга (например, `type: home` вместо `cart`). |
| **Duplicate Events** | Слишком много показов. | `sendContentEngagement` вызывается в методе `build()` или в цикле. Вынесите его в `initState` или используйте `VisibilityDetector` с флагом `_impressionSent`. |
