#
Гайд: Прямая API-интеграция для Flutter
Этот гайд предназначен для опытных Flutter-разработчиков, которым требуется полный контроль над интеграцией с Gravity Field через прямые вызовы API. В отличие от стандартной интеграции с помощью Flutter SDK, которая автоматически управляет отображением UI, этот подход даёт вам свободу создавать полностью кастомные интерфейсы.
Взамен вы берёте на себя ответственность за управление HTTP-запросами, состоянием, хранением идентификаторов пользователя и, что особенно важно, за отправку событий о взаимодействии (engagement).
#
1. Введение
#
Когда выбирать прямую API-интеграцию?
- Полный контроль над UI: Вы хотите создавать уникальные виджеты и анимации, которые невозможно реализовать стандартными шаблонами SDK.
- Сложная логика состояния: Ваше приложение использует продвинутые техники управления состоянием (BLoC, Riverpod), и вы хотите интегрировать данные от Gravity Field в существующую архитектуру.
- Минимализм: Вы предпочитаете не добавлять SDK как зависимость и работать с API напрямую.
#
2. Подготовка к работе
#
HTTP-клиент
Для выполнения HTTP-запросов мы рекомендуем использовать пакет dio. Он предоставляет удобный API для работы с Interceptors, таймаутами и обработкой ошибок.
Добавьте dio в ваш pubspec.yaml:
dependencies:
dio: ^5.8.0+1 # Используйте актуальную версию
Все запросы к API Gravity Field должны содержать заголовок Authorization с вашим API-ключом. Базовый URL для всех запросов: https://evs-01.gravityfield.ai/v2.
Пример настройки клиента Dio:
import 'package:dio/dio.dart';
class ApiClient {
final Dio dio;
final String apiKey;
final String sectionId;
ApiClient({required this.apiKey, required this.sectionId})
: dio = Dio(
BaseOptions(
baseUrl: 'https://evs-01.gravityfield.ai/v2',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 20),
headers: {
'Content-Type': 'application/json',
},
),
) {
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $apiKey';
return handler.next(options);
},
),
);
}
// ... методы для вызова API
}
#
3. Полная схема взаимодействия
Весь цикл интеграции, от идентификации пользователя до отслеживания клика, выглядит следующим образом:
sequenceDiagram
participant App as Flutter App
participant API as Gravity API
App->>API: 1. Отправка события (POST /v2/visit)
Note right of App: Отправляем с `uid` и `ses` (если есть)
API-->>App: 200 OK<br/>Ответ содержит `user` (с uid/ses) и `campaigns` (с campaignId)
Note right of App: Сохраняем `uid` на устройстве, `ses` в памяти
alt Если есть campaignId
App->>API: 2. Запрос контента (POST /v2/choose)
API-->>App: 200 OK<br/>Ответ содержит JSON контента и массив `events` с URL для отслеживания
Note right of App: Приложение рендерит UI из JSON
App->>API: 3. Отправка события показа (GET на URL из `events` с type="impression")
API-->>App: 204 No Content (Показ засчитан)
User->>App: Пользователь кликает по элементу
App->>API: 4. Отправка события клика (GET на URL из `events` с type="click")
API-->>App: 204 No Content (Клик засчитан)
end
#
4. Шаг 1: Отправка данных (контекст и события)
Первый шаг — сообщить Gravity Field о действиях пользователя (просмотр экрана, покупка и т.д.) и получить в ответ uid, ses и список ID активных кампаний.
#
Управление идентификаторами uid и ses
uid: Уникальный идентификатор пользователя. Его необходимо сохранять на устройстве (например, вSharedPreferences) и использовать между сессиями.ses: Идентификатор текущей сессии. Его нужно хранить в памяти на время работы приложения. При перезапуске приложения он должен бытьnull, чтобы сервер сгенерировал новый.
#
Отслеживание контекста экрана (вызов /visit)
Эндпоинт /visit используется для отслеживания просмотров экранов (screenview). Это основной способ сообщить платформе, где находится пользователь, и является триггером для запуска кампаний, привязанных к контексту экрана.
#
Объект PageContext
Ключевым объектом в запросе является ctx (PageContext), который описывает текущий экран.
#
Типы контекста ContextType
#
Пример кода для вызова /visit
Future<Map<String, dynamic>> trackVisit(String? uid, String? ses) async {
final data = {
'sec': sectionId,
'device': {
'userAgent': 'YourApp/1.0.0 (Dart; Flutter)',
},
'type': 'screenview',
'user': {
'uid': uid,
'ses': ses,
},
// Пример PageContext для экрана продукта
'ctx': {
'type': 'PRODUCT',
'data': ['product-sku-123'],
'location': 'app://product/123',
'attributes': {
'is_premium_user': true,
}
},
'options': {},
};
final response = await dio.post('/visit', data: data);
// ... обработка ответа ...
return response.data;
}
#
Отслеживание действий пользователя (вызов /event)
Эндпоинт /event используется для отслеживания ключевых бизнес-событий (покупка, добавление в корзину, логин) или любых других кастомных действий. Эти события могут служить триггером для кампаний или использоваться в аналитике и сегментации.
#
Стандартные типы событий
Платформа предоставляет набор стандартных событий с предопределенной структурой.
#
Кастомные события (CustomEvent)
Для отслеживания любых других действий используйте CustomEvent.
type: Уникальный системный тип события (например,survey-completed-v1).name: Человекочитаемое имя (например, «Опрос пройден»).properties: Дополнительные параметры в форматеMap<String, String>.
#
Пример кода для вызова /event
В теле запроса /event передается массив data, содержащий один или несколько объектов событий.
Future<Map<String, dynamic>> trackPurchaseEvent(String? uid, String? ses) async {
final purchaseEvent = {
'type': 'purchase-v1',
'name': 'Purchase',
'uniqueTransactionId': 'ORDER-12345',
'value': 2550.75,
'currency': 'RUB',
'cart': [
{'productId': 'sku-123', 'quantity': 1, 'itemPrice': 1000.50},
{'productId': 'sku-456', 'quantity': 2, 'itemPrice': 775.125},
],
};
final data = {
'sec': sectionId,
'device': { /* ... */ },
'user': { 'uid': uid, 'ses': ses },
'ctx': {
'type': 'OTHER',
'data': ['checkout_success'],
'location': 'app://checkout/success',
},
'data': [
purchaseEvent // Массив с одним событием покупки
],
'options': {},
};
final response = await dio.post('/event', data: data);
// ... обработка ответа ...
return response.data;
}
#
5. Шаг 2: Получение и рендеринг контента кампании (вызов /choose)
Если на предыдущем шаге вы получили campaignId, запросите контент этой кампании с помощью эндпоинта /choose.
#
Общая структура ответа
Ответ /choose имеет сложную иерархическую структуру. Ключевые данные для рендеринга находятся по следующему пути: data[0].payload[0].contents[0].
data: Массив, соответствующий запрошенным кампаниям.payload: Массив вариаций для кампании (в A/B тесте их может быть несколько).contents: Массив контентных блоков внутри вариации.decisionId: Уникальный ID, который необходимо использовать для отслеживания взаимодействий.
#
Структура контента (CampaignContent)
Объект contents[0] содержит все необходимое для рендеринга.
#
Структура variables и elements
Если contentType равен json, основной контент для рендеринга находится в variables.elements. Это массив объектов, описывающих UI-элементы.
Свойство onClick содержит action (например, follow_url) и url или deeplink для выполнения действия.
#
Структура products и slots
Если contentType равен products (или в elements есть products-container), данные о товарах находятся в products.slots.
slots: Массив объектов, каждый из которых представляет товар.slot.item: Объект с данными о товаре из вашего продуктового фида (sku,name,price,imageUrlи т.д.).slot.slotId: Уникальный ID товара в рамках данной выдачи. Используется для отслеживания кликов по конкретному товару.
#
Пример кода для вызова /choose
После того как вы получили campaignId из ответа на /visit или /event, вы можете запросить полный контент кампании. Запрос должен содержать тот же контекст (user, ctx, device), что и исходный запрос, который вернул campaignId.
Future<Map<String, dynamic>> getCampaignContent({
required String campaignId,
required String? uid,
required String? ses,
}) async {
final data = {
'sec': sectionId,
'device': {
'userAgent': 'YourApp/1.0.0 (Dart; Flutter)',
},
'user': {
'uid': uid,
'ses': ses,
},
// Контекст страницы, на которой будет показана кампания
'ctx': {
'type': 'PRODUCT',
'data': ['product-sku-123'],
'location': 'app://product/123',
},
'data': [
{
'campaignId': campaignId,
// Опционально: можно запросить только SKU или определенные поля
'option': {
'skusOnly': false,
'fields': ['name', 'price', 'imageUrl'],
}
}
],
'options': {
'isReturnCounter': false, // Не возвращать сработавшие условия и сегменты
'isReturnUserInfo': false, // Не включать в ответ подробную информацию о пользователе
'isReturnAnalyticsMetadata': true, // Получить decisionId и другие метаданные для аналитики
'isImplicitPageview': false, // Не фиксировать просмотр страницы этим запросом
'isImplicitImpression': true, // Зафиксировать показ кампании сразу при получении
'isBuildEngagementUrl': true, // Обязательно для получения URL для отслеживания взаимодействий
},
};
final response = await dio.post('/choose', data: data);
// ... обработка ответа для рендеринга UI ...
return response.data;
}
#
6. Шаг 3: Отслеживание взаимодействий (Engagement)
Отслеживание взаимодействий — критически важный шаг для аналитики и A/B-тестов. Без отправки этих событий платформа не сможет измерить эффективность кампаний. Для этого используются URL из массива events в ответе /choose.
#
Механизм отслеживания
В отличие от Server-to-Server API, мобильный API (v2) использует более простой и производительный механизм. Ответ на запрос /choose для каждой кампании содержит массив events. Каждый элемент этого массива — это объект с типом события и готовыми URL для его отслеживания.
Чтобы зафиксировать взаимодействие, вашему приложению достаточно отправить простой GET-запрос на соответствующий URL.
#
Структура массива events
Массив events находится внутри каждого объекта contents в ответе /choose. Он может выглядеть так:
// Фрагмент ответа /choose
// ...
"contents": [
{
"contentId": "...",
// ... другие поля контента
"events": [
{
"type": "impression",
"urls": [
"https://evs-01.gravityfield.ai/engagement?type=IMP&decisionId=..."
]
},
{
"type": "visible_impression",
"urls": [
"https://evs-01.gravityfield.ai/engagement?type=WRIMP&decisionId=..."
]
},
{
"type": "click",
"urls": [
"https://evs-01.gravityfield.ai/engagement?type=CLICK&decisionId=..."
]
}
]
}
]
// ...
Для товарных рекомендаций (products.slots) структура аналогична, но events находятся внутри каждого slot.
#
Схема взаимодействия
sequenceDiagram
participant App as Flutter App
participant API as Gravity API
App->>API: 1. Запрос контента (POST /v2/choose)
API-->>App: 200 OK<br/>Ответ содержит JSON контента и массив `events` с URL для отслеживания
Note right of App: Приложение рендерит UI из JSON
App->>API: 2. Отправка события показа (GET на URL из `events` с type="impression")
API-->>App: 204 No Content (Показ засчитан)
User->>App: Пользователь кликает по элементу
App->>API: 3. Отправка события клика (GET на URL из `events` с type="click")
API-->>App: 204 No Content (Клик засчитан)
#
Пример кода для извлечения и вызова URL
Эта функция поможет найти нужный URL в массиве events и отправить по нему GET-запрос.
// Функция для отправки GET-запроса по URL
Future<void> trackEngagementUrl(String url) async {
try {
// Используем отдельный экземпляр Dio без базового URL и interceptors
await Dio().get(url);
print('Engagement sent: $url');
} catch (e) {
print('Failed to trigger engagement event for $url: $e');
}
}
// Функция для поиска URL и его вызова
void processEngagement({
required List<dynamic> events,
required String eventType,
}) {
final event = events.firstWhere(
(e) => e['type'] == eventType,
orElse: () => null,
);
if (event != null && event['urls'] is List && (event['urls'] as List).isNotEmpty) {
String url = (event['urls'] as List).first;
trackEngagementUrl(url);
}
}
// --- Пример использования ---
// 1. Отправка показа всего виджета (после рендеринга)
// final contentEvents = chooseResponse['data'][0]['payload'][0]['contents'][0]['events'];
// processEngagement(events: contentEvents, eventType: 'impression');
// 2. Отправка клика по конкретному товару (в onTap)
// final slot = products['slots'][index];
// final slotEvents = slot['events'];
// processEngagement(events: slotEvents, eventType: 'click');
#
Когда какие события вызывать
#
Сценарий 1: Кампания без товаров (например, баннер)
impression: Сразу после рендеринга контента (используйтеeventsизcontents[0]).visible_impression: Когда баннер впервые становится видимым во вьюпорте (используйтеeventsизcontents[0]).click: В обработчикеonTap/onPressedдля интерактивного элемента (используйтеeventsизcontents[0]).
#
Сценарий 2: Кампания с товарными рекомендациями
impression(виджет): Сразу после рендеринга всего виджета (используйтеeventsизcontents[0]).visible_impression(виджет): Когда весь виджет становится видимым (используйтеeventsизcontents[0]).visible_impression(товар): Когда конкретный товар (slot) становится видимым при скролле (используйтеeventsизslot).click(товар): При нажатии на конкретный товар (используйтеeventsизslot).
#
7. FAQ и лучшие практики
В: Как обрабатывать ошибки API?
О: Всегда оборачивайте вызовы API в try-catch. В случае ошибки (например, нет сети или сервер вернул 500), показывайте пользователю UI по умолчанию (fallback). Не позволяйте ошибкам API нарушать работу вашего приложения.
В: Какие таймауты использовать?
О: Рекомендуется устанавливать таймауты на соединение (5–10 секунд) и получение ответа (15–20 секунд), чтобы не заставлять пользователя ждать слишком долго.
В: Что делать, если API не вернул кампанию?
О: Это нормальная ситуация. Если массив campaigns в ответе /visit пуст, или /choose возвращает пустой data, это означает, что для данного пользователя сейчас нет активных кампаний. В этом случае также показывайте UI по умолчанию.
#
Спецификация объектов ответа API
Данная спецификация актуальна для Flutter SDK версии 0.9.8. Структура ответа может изменяться в будущих версиях.
Этот раздел описывает структуру JSON-объектов, которые возвращают эндпоинты API Gravity Field. Основным источником для этой спецификации служат модели данных Flutter SDK.
#
Ответ эндпоинта /choose
Эндпоинт /choose возвращает наиболее сложную структуру, содержащую все данные, необходимые для отображения кампании. Корневым объектом является ContentResponse.
#
ContentResponse (Корневой объект)
user(Object): Объект с идентификаторами пользователя. См.спецификацию объекта User .data(List<Object>): Список объектов кампаний. См.спецификацию объекта Campaign .
#
Campaign
selector(String, nullable): Селектор, по которому была запрошена кампания (если применимо).payload(List<Object>): Список вариаций кампании. Обычно содержит один элемент. См.спецификацию объекта CampaignVariation .
#
CampaignVariation
campaignId(String): Уникальный идентификатор кампании.experienceId(String): Идентификатор сценария (experience).variationId(String): Идентификатор вариации.decisionId(String): Уникальный идентификатор решения о показе. Используется для отслеживания взаимодействий.contents(List<Object>): Список контентных блоков. Обычно содержит один элемент. См.спецификацию объекта CampaignContent .
#
CampaignContent
contentId(String): Уникальный идентификатор блока контента.templateSystemName(String, enum, nullable): Системное имя шаблона (например,snackbar-1,snackbar-2).deliveryMethod(String, enum): Способ отображения. Возможные значения:modal: Модальное окно.snackbar: Уведомление внизу экрана.bottom_sheet: Шторка снизу.fullscreen: Полноэкранный режим.inline: Встраиваемый в верстку контент.
contentType(String): Тип контента (например,json,products,banner).variables(Object): Объект, содержащий элементы UI и их стили. См.спецификацию объекта Variables .products(Object, nullable): Объект с товарными рекомендациями. См.спецификацию объекта Products .events(List<Object>, nullable): Список объектов с URL для отслеживания взаимодействий. См.спецификацию объекта Event для контента .
#
Variables
Объект, описывающий UI кампании.
frameUI(Object, nullable): Стили и элементы рамки (контейнера) кампании. См.спецификацию объекта FrameUI .elements(List<Object>): Список UI-элементов внутри кампании. См.спецификацию объекта Element .onLoad(Object, nullable): Действие, которое нужно отследить при загрузке контента.onImpression(Object, nullable): Действие при показе.onVisibleImpression(Object, nullable): Действие при попадании в зону видимости.onClose(Object, nullable): Действие при закрытии.
#
FrameUI
container(Object): Стили основного контейнера.style(Object): См.спецификацию объекта Style .
close(Object, nullable): Описание кнопки закрытия.image(String, nullable): URL изображения для иконки закрытия.onClick(Object, nullable): Действие при клике. См.спецификацию объекта OnClick .style(Object): Стили для кнопки закрытия. См.спецификацию объекта Style .
#
Element
Описывает один UI-элемент (текст, кнопка, изображение).
type(String, enum): Тип элемента. Возможные значения:imagetextbuttonspacer(пустое пространство)products-container(контейнер для товарных рекомендаций)
text(String, nullable): Текст для элементовtextиbutton.src(String, nullable): URL для элементаimage.style(Object, nullable): Стили элемента. См.спецификацию объекта Style .onClick(Object, nullable): Действие при клике. См.спецификацию объекта OnClick .
#
Style
Объект со стилями, аналогичными CSS. Поля являются опциональными.
backgroundColor(String, nullable): Цвет фона (например,#FFFFFF).pressColor(String, nullable): Цвет при нажатии.outlineColor(String, nullable): Цвет обводки.cornerRadius(Double, nullable): Радиус скругления углов.fontSize(Double, nullable): Размер шрифта.fontWeight(String, nullable): Насыщенность шрифта (например,400,700).textColor(String, nullable): Цвет текста.fit(String, enum, nullable): Режим масштабирования для изображений (cover,containи т.д.).contentAlignment(String, enum, nullable): Выравнивание контента (start,center,end).size(Object, nullable): Размеры элемента.width(Double, nullable)height(Double, nullable)
margin/padding(Object, nullable): Внешние/внутренние отступы.left,right,top,bottom(Double)
positioned(Object, nullable): Абсолютное позиционирование.left,right,top,bottom(Double, nullable)
#
OnClick
Описывает действие, которое происходит при нажатии на элемент.
action(String, enum): Тип действия. Возможные значения:follow_url: Переход по внешней ссылке.follow_deeplink: Переход по диплинку.copy: Копирование данных в буфер обмена.close: Закрытие кампании.request_push: Запрос разрешения на push-уведомления.request_tracking: Запрос разрешения на отслеживание (ATT).
url(String, nullable): URL для действияfollow_url.deeplink(String, nullable): Диплинк для действияfollow_deeplink.copyData(String, nullable): Данные для копирования для действияcopy.closeOnClick(Boolean): Закрывать ли кампанию после выполнения действия. По умолчаниюtrue.
#
Products
Объект, содержащий товарные рекомендации.
strategyId(String): ID использованной рекомендательной стратегии.name(String): Название стратегии.fallback(Boolean):true, если была использована резервная (fallback) стратегия.slots(List<Object>): Список слотов с товарами. См.спецификацию объекта Slot .
#
Slot
Один слот с рекомендованным товаром.
item(Object): Объект с данными о товаре. См.спецификацию объекта Item .fallback(Boolean):true, если товар был добавлен из резервной стратегии.strId(Int): ID алгоритма внутри стратегии.slotId(String): Уникальный ID слота для отслеживания взаимодействий.events(List<Object>, nullable): Список URL для отслеживания взаимодействий с этим товаром. См.спецификацию объекта Event для продукта .
#
Item
Данные о товаре, как они представлены в вашем продуктовом фиде. Набор полей может отличаться.
sku(String): Уникальный идентификатор товара.groupId(String, nullable): Идентификатор группы товаров (например, одна модель в разных цветах).name(String): Название товара.price(String): Цена.url(String): URL страницы товара.imageUrl(String, nullable): URL основного изображения.oldPrice(String, nullable): Старая цена (для скидок).brand(String, nullable): Бренд.inStock(Boolean, nullable): Наличие.categories(List<String>, nullable): Список категорий.keywords(List<String>, nullable): Ключевые слова.
#
Event для контента
type(String, enum): Тип события (impression,visible_impression,close,clickи т.д.). СоответствуетAction.urls(List<String>): Список URL, на которые нужно отправить GET-запрос для отслеживания события.
#
Event для продукта
type(String, enum): Тип события (impression,visible_impression,click).urls(List<String>): Список URL для отслеживания.
#
Ответ эндпоинтов /visit и /event
Эти эндпоинты возвращают более простую структуру CampaignIdsResponse, содержащую ID кампаний, которые нужно запросить через /choose.
#
CampaignIdsResponse (Корневой объект)
user(Object): Объект с идентификаторами пользователя. См.спецификацию объекта User .campaigns(List<Object>): Список ID кампаний для активации. См.спецификацию объекта CampaignId .
#
CampaignId
campaignId(String): ID кампании, которую нужно запросить через эндпоинт/choose.trigger(String): Тип триггера, который активировал кампанию.
#
Общие объекты
#
User
uid(String, nullable): Уникальный ID пользователя, присвоенный Gravity Field.custom(String, nullable): Ваш внутренний ID пользователя.ses(String, nullable): ID текущей сессии.attributes(Map<String, String>, nullable): Дополнительные атрибуты пользователя.