#
Гайд: Прямая 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 товара в рамках данной выдачи. Используется для отслеживания кликов по конкретному товару.
// Концептуальный пример рендеринга
// ...
final content = snapshot.data!['data'][0]['payload'][0]['contents'][0];
final elements = content['variables']['elements'] as List;
final products = content['products'];
return Column(
children: elements.map((element) {
switch (element['type']) {
case 'text':
return Text(element['text']);
case 'button':
return ElevatedButton(onPressed: () { /* ... */ }, child: Text(element['text']));
case 'products-container':
return buildProductCarousel(products); // Ваша функция для рендеринга карусели
default:
return SizedBox.shrink();
}
}).toList(),
);
#
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): Тип элемента. Возможные значения:image
text
button
spacer
(пустое пространство)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): Дополнительные атрибуты пользователя.