#
SDK Gravity Field (Flutter)
Gravity Field SDK — это лёгкий клиент для интеграции мобильных приложений (iOS / Android) с платформой персонализации и A/B-тестирования. Он позволяет запускать кампании без необходимости реализовывать собственную логику таргетинга, выбора и аналитики.
SDK работает по принципу "тонкого клиента": все решения принимает сервер (бекенд Gravity Field), а SDK:
- передаёт контекст текущего экрана,
- получает кампании и их содержимое в виде JSON или встроенных шаблонов,
- активирует показ
inline
иin-app
кампаний - трекает взаимодействия пользователя (просмотры, клики, покупки и т.д.),
- и помогает фиксировать конверсии для аналитики и обучения моделей.
📦 Эта документация предназначена для мобильных разработчиков и инженеров, которые интегрируют SDK в приложение.
#
Добавление библиотеки в проект
- Из корня проекта вызовите команду:
flutter pub add gravity_sdk
- После добавления плагина в файле
pubspec.yaml
появится строка с зависимостью:
dependencies:
gravity_sdk: ^1.0.0
- Добавьте импорт:
import 'package:gravity_sdk/gravity_sdk.dart';
#
Инициализация SDK
Для работы SDK необходимо провести базовую инициализацию, которая включает регистрацию секции, авторизацию через API-ключ и настройку поведения получения кампаний.
#
initialize(...)
Метод принимает обязательные параметры:
void initialize({
required String section,
required String apiKey,
ProductWidgetBuilder? productWidgetBuilder,
ActionsCallback? actionsCallback,
})
section
— ID секции (выдаётся системой)
apiKey
— API-ключ (выдаётся системой)
productWidgetBuilder
- функция, задающая внешний вид товарной карточки в виджете. Определяется на стороне приложения и используется SDK как шаблон для отображения рекомендаций.
actionsCallback
- это глобальный обработчик всех событий взаимодействия с кампаниями и контентом в SDK. Механизм, через который SDK Gravity Field сообщает приложению о событиях, происходящих в кампании.
Полниый список событий, которые могут прилетать в actionsCallback
:
Используйте, если вам нужно:
- Обработка пользовательских действий Например, переход по deeplink
FollowDeeplinkEvent
или открытие URLFollowUrlEvent
. - Обработка форм При
SubmitEvent
приложение получает введённые данные и само решает, что с ними делать: отправить на сервер, показать уведомление, переключить экран и т.д. - Отправка событий в аналитику Например:
ImpressionEvent
,VisibleImpressionEvent
,ProductImpressionEvent
— удобно логировать показ кампаний и товаров в собственную аналитику (Firebase, AppMetrica, Sentry и т.д.) - Встроенные сценарии приложения Использование событий SDK как триггеров для запуска внутренних функций — например, после
RequestPushEvent
показать системный экран настройки уведомлений.
#
Примеры обработки actionsCallback
FollowUrlEvent — открыть ссылку в браузере
import 'package:url_launcher/url_launcher.dart';
// ...
actionsCallback: (event) async {
if (event is FollowUrlEvent) {
final uri = Uri.tryParse(event.url);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
FollowUrlEvent — открыть ссылку во встроенном WebView
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
// ...
actionsCallback: (event) {
if (event is FollowUrlEvent) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: Text('Просмотр')),
body: WebView(
initialUrl: event.url,
javascriptMode: JavascriptMode.unrestricted,
),
),
),
);
}
}
Вы можете заменить Scaffold на кастомный экран, если у вас есть свой дизайн для WebView.
FollowDeeplinkEvent — переход по deeplink
import 'package:url_launcher/url_launcher.dart';
// ...
actionsCallback: (event) async {
if (event is FollowDeeplinkEvent) {
final uri = Uri.tryParse(event.deeplink);
if (uri != null && await canLaunchUrl(uri)) {
await launchUrl(uri);
}
}
}
Deeplink может открывать другое приложение или внутренний экран.
RequestPushEvent — запрос разрешения + переход в настройки
import 'package:permission_handler/permission_handler.dart';
// ...
actionsCallback: (event) async {
if (event is RequestPushEvent) {
final status = await Permission.notification.status;
if (!status.isGranted) {
final result = await Permission.notification.request();
if (!result.isGranted) {
// Разрешение не получено — открываем системные настройки
await openAppSettings();
}
}
}
}
Убедитесь, что в AndroidManifest.xml и Info.plist указаны разрешения на уведомления.
#
setOptions(...)
Метод позволяет настроить дополнительные параметры, которые влияют на то, как сервер возвращает кампании и как SDK их обрабатывает.
void setOptions({Options? options, ContentSettings? contentSettings, String? proxyUrl})
class Options {
final bool isReturnCounter;
final bool isReturnUserInfo;
final bool isReturnAnalyticsMetadata;
final bool isImplicitPageview;
final bool isImplicitImpression;
final bool isBuildEngagementUrl = true;
const Options({
this.isReturnCounter = false,
this.isReturnUserInfo = false,
this.isReturnAnalyticsMetadata = false,
this.isImplicitPageview = false,
this.isImplicitImpression = true,
});
}
class ContentSettings {
final bool skusOnly;
final List<String>? fields;
const ContentSettings({
this.skusOnly = false,
this.fields,
});
}
options
contentSettings
настройки товарного контента
Таким образом, configureCampaignOptions
позволяет заранее определить структуру и поведение всех кампаний, которые будут получены через SDK
proxyUrl
Базовый URL вашего backend-прокси, через который SDK будет отправлять все запросы. Если не задан — используется endpoint Gravity Field.
#
Идентификация пользователей
Идентификация пользователя — фундамент для персонализации, аналитики и построения омниканальных профилей в Gravity Field.
Без корректной идентификации невозможно:
- Связывать действия пользователя между сеансами и устройствами,
- Персонализировать рекомендации,
- Точно собирать аналитику,
- Строить сценарии персонализации и сегментацию.
#
Варианты идентификации
Gravity Field позволяет автоматически или вручную управлять идентификацией, гибко подстраиваясь под архитектуру вашего проекта.
Gravity Field UID (автоматическая идентификация)
Когда использовать:
- Когда нет собственного механизма учёта пользователей и сессий
Как работает:
- При первом запросе к Gravity Field (например,
trackView(...)
), если нет сохранённых идентификаторов, Gravity Field автоматически создаёт уникальные идентификаторы:uid
— уникальный идентификатор пользователя, автоматически сгенерированный Gravity Fieldses
— ID сессии, автоматически сгенерированный Gravity Field
- Эти значения возвращаются в ответе и сохраняются.
- SDK будет использовать их во всех следующих запросах.
- Сессия живёт 30 минут и продлевается при активности пользователя.
После авторизации:
- Чтобы связать анонимный профиль с авторизованным, вызовите событие
login
черезtriggerEvent(...)
. - Gravity Field объединит поведение до и после логина.
Custom user id (идентификация через ID приложения)
Когда использовать:
- Если у пользователя в приложении есть собственный ID и логика управления сессиями.
Как работает:
- Разработчик вызывает
setUser(...)
.userId
— ваш собственный ID пользователя (custom ID);sessionId
— ID текущей сессии.
- SDK использует эти значения в каждом запросе.
💡 Если вы передаёте
userId
, вы обязаны также передатьsessionId
. SDK не может создатьsessionId
автоматически в этом режиме.
#
setUser(...)
Позволяет явно установить параметры пользователя и сессии, которые будут автоматически включаться во все запросы SDK к API (/visit
, /event
, /choose
, /engagement
).
// Собственный ID пользователя и сессия
void setUser(String userId, String sessionId)
#
Как SDK работает с идентификаторами
SDK автоматически управляет идентификаторами пользователя и сессии, опираясь на то, что уже сохранено локально. Используется следующая логика:
- Если вызван
setUser(...)
— SDK использует переданныеuserId
иsessionId
(режим: custom user ID). - Если
setUser(...)
не вызывался, но уже естьuid
иses
— SDK использует их (режим: Gravity Field UID). - Если ни один ID не сохранён — SDK отправляет запрос без идентификаторов. Gravity Field создаёт
uid
иses
, SDK сохраняет их и использует в дальнейшем.
#
Передача контекста
Gravity Field принимает решение о показе персонализированного контента на основе контекста страницы, который передаёт SDK.
Контекст страницы позволяет Gravity Field понять:
- Где сейчас находится пользователь в приложении
- Какие товары или категории он просматривает
- В каком регионе или в каких условиях пользователь находится
#
trackView(...)
Метод отправляет событие /visit
, фиксируя просмотр экрана пользователем. Он формирует тело запроса на основе параметров, переданных клиентом, и автоматически добавленных данных от SDK.
fun trackView(
ctx: PageContext
)
Параметры:
- isReturnCounter
- isReturnUserInfo | SDK передает в запросе |
Пример:
sdk.trackView(
ctx = mapOf(
"type" to "CART",
"data" to listOf("cart"),
"location" to "app://cart",
"lng" to "msk_warehouse",
"attributes" to mapOf("segment" to "loyal_users")
)
)
#
PageContext
Для унификации передачи контекста страницы во все методы SDK предлагается использовать отдельную модель:
data class PageContext(
val type: String,
val data: List<String>,
val location: String,
val lng: String? = null,
val attributes: Map<String, Any>? = null
)
💡 Типы контекста и соответсвующие для них data описаны здесь: Page context
#
Как активируются кампании на основе контекста страницы
Когда приложение отправляет событие просмотра страницы через trackView(...)
, SDK передаёт информацию о текущем контексте в Gravity Field. На основе этой информации кампании могут активироваться:
- Если в Gravity Field настроена кампания, которая должна запускаться на определённый
pageView
(например, просмотр корзины, карточки товара и т.д.), - И условия кампании совпадают с отправленным контекстом страницы (
type
,data
,location
и другие параметры), - Тогда Gravity Field выберет подходящую кампанию и формирует её содержимое (например, баннер, всплывающее окно или блок рекомендаций), SDK получает готовый контент и отображает в приложение
#
Трекинг событий
Трекинг событий позволяет отправлять информацию о действиях пользователя — таких как покупка, добавление товара в корзину, авторизация и другие события. Эти данные используются Gravity Field для аналитики, построения сегментов и запуска кампаний, активируемых по событиям.
Трекинг событий применяется, когда кампании должны срабатывать не по просмотру страницы, а по пользовательскому действию, и также для сбора аналитики.
События фиксируются для того, чтобы измерить поведение пользователя в разных вариациях кампаний: например, узнать, какой из вариантов чекаута приводит к более высокой конверсии. Это позволяет анализировать эффективность персонализации и принимать решения на основе данных.
Платформа поддерживает как системные (предопределённые) события, так и произвольные пользовательские события.
- Системные события — это события с зафиксированными
type
, которые платформа распознаёт и может использовать для активации кампаний или анализа (например,purchase
,add_to_cart
,login
). - Пользовательские события — разработчик может отправлять любые события с кастомным
type
. Такие события будут использоваться для аналитики и построения собственных отчётов или логики в Gravity Field.
#
triggerEvent(...)
fun triggerEvent(
events: List<TrackedEvent>,
ctx: PageContext
)
Параметры:
- isReturnCounter
- isReturnUserInfo | SDK передает в запросе |
Пример:
sdk.triggerEvent(
events = listOf(
TrackedEvent(
type = "purchase",
name = "Успешная покупка",
value = 189.97,
quantity = 3,
productId = "sku-123",
cart = listOf(
CartItem(
productId = "sku-123",
quantity = 2,
itemPrice = 59.99
),
CartItem(
productId = "sku-456",
quantity = 1,
itemPrice = 69.99
)
),
customProps = mapOf(
"promo_code" to "SPRING25",
"payment_method" to "apple_pay"
),
uniqueTransactionId = "txn-000123456",
cuid = "external-customer-id-987",
cuidType = "customer_id",
hashedEmail = "c02b82b72772339fa47f1b1f2940f78f20e4a4b6d98ea9d3c244f7c8e637eabc"
)
),
ctx = PageContext(
type = "CHECKOUT",
data = listOf("checkout"),
location = "app://checkout"
)
)
#
TrackedEvent
Соответствие типов эвентов и параметров
#
CartItem
#
Как активируются кампании по событию
Когда приложение отправляет событие через triggerEvent(...)
, SDK передаёт информацию о событии и текущем контексте в Gravity Field. На основе этой информации кампании могут активироваться:
- Если в Gravity Field настроена кампания, которая должна запускаться на определённый
trackedEvent
(например, добавление в корзину, покупку и т.д.), - И условия кампании совпадают с отправленным контекстом страницы (
type
,data
,location
и другие параметры), - Тогда Gravity Field выберет подходящую кампанию и формирует её содержимое (например, баннер, всплывающее окно или блок рекомендаций), SDK получает готовый контент и отображает в приложение
#
Активация кампаний
#
Inline-блоки (встраиваемые кампании в UI)
Кампания встраивается в существующую структуру экрана приложения — но только если условия показа, заданные на платформе Gravity Field, выполнены.
GravityInlineWidget
SDK запрашивает кампанию по selector
, передаёт контекст текущей страницы, и если сервер вернёт релевантный контент — он будет отображён в заданном UI-элементе.
GravityInlineWidget({
required String selector,
String? placeholderId,
double? width,
double? height,
Map<String, dynamic>? attributes,
})
selector
задается в настройках кампании, в личном кабинете Gravity Field.
placeholderId
задается в настройках вариации кампании, в лично кабинете Gravity Field
💡 Если условия показа не выполнены (например, пользователь не входит в целевую группу), блок не будет отрисован.
📌 Подходит для: баннеров, товарных рекомендаций, УТП блоков.
Пример:
class InlineScreen extends StatelessWidget {
const InlineScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Inline Screen'),
),
body: Column(
children: [
GravityInlineWidget(
width: 200,
height: 200,
selector: 'homepage-rec-widget'
),
SizedBox(height: 36,),
GravityInlineWidget(
selector: 'homepage-banner',
)
],
),
);
}
}
#
[WIP]Кастомизация внешнего вида товарных рекомендаций
TODO
#
In-app кампании (всплывающие окна, модальные экраны)
Кампания отображается поверх интерфейса — это может быть snackbar, bottom sheet, или модальное окно. Такие кампании активируются автоматически, если условия показа, настроенные в платформе Gravity Field, выполнены.
Когда и как происходит активация:
- SDK отправляет запрос через
trackView(...)
илиtriggerEvent(...)
с текущим контекстом страницы и события - Сервер Gravity Field анализирует условия:
- Целевая страница (
ctx.data
) - Условия сегментации (аудитория, сегменты, сессия, гео и т.д.)
- Частота показов, лимиты, включённость в эксперимент
- Целевая страница (
- Если кампания активна и все условия выполнены — сервер возвращает кампанию
- SDK автоматически отображает in-app сообщение через встроенные шаблоны
📌 Подходит для:
- Всплывающих предложений (например, «вернитесь в корзину»)
- Промо после события (
purchase
,login
,logout
) - Модальных сообщений при входе на экран
- A/B-тестов с воронками входа / выхода
#
Отображение JSON-кампаний на стороне приложения
В отличие от in-app
и inline
кампаний, которые SDK может отрисовать автоматически (например, баннеры, модалки, ленты товаров по шаблонам), JSON-кампании не имеют визуального шаблона. Gravity Field возвращает только структурированные данные, и отображение полностью реализуется на стороне приложения.
Есть два способа получить кампанию:
Вариант 1: Через getCampaign(...)
Метод getCampaign(...)
используется, когда кампания нужна по конкретному selector
или groupSelector
.
fun getCampaign(
selector: String? = null,
groupSelector: String? = null,
page: PageContext? = null,
onResult: (campaigns: List<Campaign>) -> Unit
)
📌 Подходит для:
- Получения кампаний заранее (например, до отрисовки экрана)
- Встраивания кампаний в кастомные блоки
Вариант 2: Через actionsCallback
после события или просмотра экрана
Если вам нужно вручную обработать кампанию (например, отрисовать попап или вставить блок) — используйте параметр actionsCallback
при инициализации SDK.
Событие с типом Load
приходит сразу после отправки события (triggerEvent(...)
) или просмотра страницы (trackView(...)
). Вы можете проверить selector
и решить, нужно ли отображать кампанию.
GravityFieldSdk.initialize(
sectionId: 'abc123',
apiKey: 'gf_xyz...',
actionsCallback: (event) {
if (event.type == 'LoadEvent' && event.selector == 'custom_popup') {
// кастомная логика показа кампании
showCustomPopup(event.campaign);
}
},
);
📌 Такой подход позволяет:
- Получать кампании в момент события или просмотра экрана
- Гибко управлять отображением кампаний вручную
- Тестировать варианты крупных блоков в приложении, например, новый дизайн карточки товара
#
Campaign
Объект Campaign
представляет собой активированную персонализированную кампанию, полученную от сервера Gravity Field через getCampaign(...)
, trackView(...)
или triggerEvent(...)
data class Campaign(
val selector: String,
val groupSelector: String?,
val decisionId: String,
val contents: List<CampaignContent>
)
Почему contents — это список
Одна кампания может содержать несколько визуальных элементов, например:
- Баннер + товарная лента
- Модальное окно + напоминание
- Несколько последовательных шагов (например, туториал)
Каждый элемент описывается через CampaignContent
.
#
CampaignContent
data class CampaignContent(
val contentId: String,
val deliveryMethod: String,
val placeholderId: String,
val step: Integer,
val templateId: String,
val templateSystemName: String,
val contentType: String,
val custom: JSONObject,
val products: ProductsBlock?
)
#
ProductsBlock
data class ProductsBlock(
val strategyId: String,
val name: String?,
val pageNumber: Int?,
val countPages: Int?,
val fallback: Boolean,
val slots: List<ProductSlot>
)
#
ProductSlot
data class ProductSlot(
val item: JSONObject,
val fallback: Boolean,
val strId: Int,
val slotId: String,
)
#
Трекинг взаимодействий (engagement) с кампанией
Engagement-события для кампаний позволяют Gravity Field анализировать поведение пользователя в деталях:
- Какой контент был показан и как он повлиял на поведение
- Какая вариация кампании эффективнее: какой баннер чаще просматривают, на какой товар чаще кликают, что влияет на покупку
- Понимание того, какие элементы UI реально видны (
WRIMP
), позволяет фильтровать «слепые зоны» и улучшать UX - Фиксация кликов и показов даёт системе обратную связь: что работает, а что нет
in-app
и inline(баннерных)
кампании — SDK автоматически отслеживает показ, клики и видимость блоков.
json
кампании — SDK не знает, когда блок был показан, видим или кликнут. Поэтому вы должны вручную вызывать трекинг нужных событий.
inline(товарные)
кампании — SDK автоматически отслеживает показ, так как карточки отрисовывает приложение через productWidgetBuilder
Поэтому вы должны вручную вызывать трекинг кликов(PCLICK
) по товарам.
SDK отдаёт готовые методы для их трекинга — вам не нужно вручную формировать запросы.
CampaignContent
содержит методы, по которым Gravity Field отслеживает взаимодействие пользователя:
ProductSlot
содержит методы, по которым Gravity Field отслеживает взаимодействие пользователя c товаром:
🔗 Все эти события привязаны к decisionId
кампании и, при необходимости, к contentId
и slotId
товара.
Пример: отображение и трекинг УТП-блока
Шаг 1: Получение кампании
sdk.getCampaign(
selector = "checkout_usp_banner",
page = PageContext(
type = "CART",
data = listOf("cart"),
location = "app://cart"
),
onResult = { campaigns ->
campaigns.firstOrNull()?.let { campaign ->
val content = campaign.contents.firstOrNull()
if (content != null) {
renderUspBlock(content)
}
}
}
)
Пример JSON-контента кампании
{
"title": "Бесплатная доставка сегодня!",
"text": "Оформите заказ до 23:59 и получите бесплатную доставку.",
"button": {
"text": "Оформить заказ",
"action": "navigate",
"target": "app://checkout"
}
}
Шаг 2: Рендер блока
fun renderUspBlock(content: CampaignContent) {
val json = content.custom
val title = json.optString("title")
val text = json.optString("text")
val buttonText = json.optJSONObject("button")?.optString("text")
val target = json.optJSONObject("button")?.optString("target")
uspTitleView.text = title
uspDescriptionView.text = text
uspButton.text = buttonText
// Трекинг показа
content.trackIMP()
// Отложенный трекинг видимости (> 1с на экране)
if (isVisibleForMoreThan1Second(uspBlock)) {
content.trackWRIMP()
}
// Обработка нажатия
uspButton.setOnClickListener {
content.trackWCLICK()
navigateTo(target)
}
}
Что в итоге делает разработчик:
- Получает кампанию по
selector
- Отрисовывает JSON как UI
- Вызывает engagement-трекинг вручную (
trackIMP
,trackWRIMP
,trackWCLICK
)
#
Все события для колбека
abstract class TrackingEvent {
final Campaign campaign;
const TrackingEvent({required this.campaign});
}
// Общие события
class LoadEvent extends TrackingEvent {
const LoadEvent({
required super.campaign,
});
}
class ImpressionEvent extends TrackingEvent {
final Content content;
const ImpressionEvent({
required super.campaign,
required this.content,
});
}
class VisibleImpressionEvent extends TrackingEvent {
final Content content;
const VisibleImpressionEvent({
required super.campaign,
required this.content,
});
}
class CloseEvent extends TrackingEvent {
final Content content;
final String source;
const CloseEvent({
required super.campaign,
required this.content,
required this.source,
});
}
class CopyEvent extends TrackingEvent {
final Content content;
final String copiedValue;
const CopyEvent({
required super.campaign,
required this.content,
required this.copiedValue,
});
}
class CancelEvent extends TrackingEvent {
final Content content;
const CancelEvent({
required super.campaign,
required this.content,
});
}
class SubmitEvent extends TrackingEvent {
final Content content;
final Map<String, dynamic> formData;
const SubmitEvent({
required super.campaign,
required this.content,
required this.formData,
});
}
class NextStepEvent extends TrackingEvent {
final Content content;
final int step;
const NextStepEvent({
required super.campaign,
required this.content,
required this.step,
});
}
class FollowUrlEvent extends TrackingEvent {
final Content content;
final Uri url;
const FollowUrlEvent({
required super.campaign,
required this.content,
required this.url,
});
}
class FollowDeeplinkEvent extends TrackingEvent {
final Content content;
final Uri deeplink;
const FollowDeeplinkEvent({
required super.campaign,
required this.content,
required this.deeplink,
});
}
class RequestPushEvent extends TrackingEvent {
final Content content;
const RequestPushEvent({
required super.campaign,
required this.content,
});
}
class RequestTrackingEvent extends TrackingEvent {
final Content content;
const RequestTrackingEvent({
required super.campaign,
required this.content,
});
}
// товарные
class ProductImpressionEvent extends TrackingEvent {
final Slot slot;
final Content content;
const ProductImpressionEvent({
required super.campaign,
required this.slot,
required this.content,
});
}