#
Гайд: A/B-тестирование с Flutter SDK
Это руководство представляет собой пошаговую инструкцию по реализации A/B-тестирования в Flutter-приложении, где варианты интерфейса управляются удаленно с помощью JSON, получаемого от Gravity Field SDK. Такой подход, известный как Server-Driven UI, обеспечивает максимальную гибкость и позволяет запускать тесты без необходимости выпускать новые версии приложения.
Цель руководства: Провести вас через весь процесс: от настройки кампании в интерфейсе Gravity Field до реализации логики в приложении и отслеживания результатов.
Что мы будем делать: В качестве примера мы создадим A/B-тест для экрана оплаты, где будем проверять различные комбинации кнопок оплаты.
- Вариант А (Контрольный): Стандартная кнопка "Оплатить картой".
- Вариант Б (Тестовый): Две кнопки: "Оплатить картой" и "Оплатить через СБП".
#
Шаг 1: Настройка кампании в Gravity Field
Первый шаг — создать кампанию, которая будет возвращать JSON с описанием нашего UI.
- Перейдите в раздел Campaigns → API Campaigns и нажмите Создать кампанию.
- Выберите тип кампании Custom JSON. Этот тип позволяет вернуть данные в произвольном JSON-формате.
- Задайте API-селектор (например,
payment_screen_options
). Этот селектор будет использоваться в коде приложения для получения кампании. - Создайте две вариации: "Вариант А" и "Вариант Б".
- В каждой вариации определите JSON-объект, который описывает контент.
Вариант А (Контрольный):
{
"title": "Выберите способ оплаты",
"buttons": [
{
"id": "card",
"text": "Оплатить картой",
"style": "primary"
}
]
}
Вариант Б (Тестовый):
{
"title": "Выберите способ оплаты",
"buttons": [
{
"id": "card",
"text": "Оплатить картой",
"style": "secondary"
},
{
"id": "sbp",
"text": "Оплатить через СБП",
"style": "primary"
}
]
}
- Настройте распределение трафика (например, 50/50) и сохраните кампанию.
💡 Важно: API-селектор — это ключ, по которому ваше приложение будет запрашивать эту конкретную кампанию. Убедитесь, что он уникален и понятен.
#
Шаг 2: Получение варианта теста в приложении
Теперь в Flutter-приложении нам нужно запросить данные кампании с помощью SDK. Для этого используется метод GravitySDK.instance.getContentBySelector(...)
.
Этот метод асинхронный и возвращает Future<ContentResponse>
. Ответ может быть успешным, содержать ошибку или быть пустым, если для пользователя нет активной кампании.
import 'package:gravity_sdk/gravity_sdk.dart';
Future<Campaign?> fetchPaymentOptions() async {
try {
final response = await GravitySDK.instance.getContentBySelector(
selector: 'payment_screen_options', // Наш API-селектор
pageContext: PageContext(
type: ContextType.cart,
data: [],
location: 'app://payment',
),
);
if (response.data.isNotEmpty) {
// Кампания найдена, возвращаем ее
return response.data.first;
} else {
// Для пользователя нет активной кампании
return null;
}
} catch (e) {
// Обработка ошибок (например, нет сети)
print('Error fetching payment options: $e');
return null;
}
}
Из ответа нам важны два элемента:
decisionId
: Уникальный идентификатор решения, который понадобится для отслеживания взаимодействий.payload
: Сам JSON-объект, который мы определили в Gravity Field.
#
Шаг 3: Рендеринг UI на основе JSON
После получения данных мы можем динамически строить интерфейс. Лучше всего для этого подходит FutureBuilder
.
#
3.1. Создание моделей данных (Data Models)
Чтобы работать с JSON было удобно и безопасно, создадим Dart-классы для наших данных.
// Модель для кнопки
class PaymentButtonModel {
final String id;
final String text;
final String style;
PaymentButtonModel({required this.id, required this.text, required this.style});
factory PaymentButtonModel.fromJson(Map<String, dynamic> json) {
return PaymentButtonModel(
id: json['id'] as String,
text: json['text'] as String,
style: json['style'] as String,
);
}
}
> 💡 Примечание: Поле для доступа к вашему JSON-объекту называется `.custom`. Это стандартное имя поля в API-ответе Gravity Field для кампаний типа Custom JSON.
// Модель для всего экрана
class PaymentScreenModel {
final String title;
final List<PaymentButtonModel> buttons;
PaymentScreenModel({required this.title, required this.buttons});
factory PaymentScreenModel.fromJson(Map<String, dynamic> json) {
var buttonList = json['buttons'] as List;
List<PaymentButtonModel> buttons =
buttonList.map((i) => PaymentButtonModel.fromJson(i)).toList();
return PaymentScreenModel(
title: json['title'] as String,
buttons: buttons,
);
}
}
#
3.2. Создание виджета с FutureBuilder
Теперь создадим виджет, который будет запрашивать данные и отображать либо состояние загрузки, либо UI на основе полученного JSON, либо UI по умолчанию (fallback).
import 'package:flutter/material.dart';
import 'package:gravity_sdk/gravity_sdk.dart';
import 'dart:convert';
class PaymentScreen extends StatefulWidget {
@override
_PaymentScreenState createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late Future<Campaign?> _campaignFuture;
@override
void initState() {
super.initState();
_campaignFuture = fetchPaymentOptions();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Оплата')),
body: FutureBuilder<Campaign?>(
future: _campaignFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
// Fallback UI: если кампания не пришла или произошла ошибка
return _buildDefaultUi();
}
// Кампания успешно загружена, строим UI на основе JSON
final campaign = snapshot.data!;
final customData = campaign.variations.first.payload.data.custom;
final Map<String, dynamic> contentJson =
customData is String ? jsonDecode(customData) : customData as Map<String, dynamic>;
final model = PaymentScreenModel.fromJson(contentJson);
return _buildDynamicUi(model, campaign);
},
),
);
}
// UI по умолчанию
Widget _buildDefaultUi() {
return Center(
child: ElevatedButton(
onPressed: () { /* ... */ },
child: Text('Оплатить картой'),
),
);
}
// Динамический UI
Widget _buildDynamicUi(PaymentScreenModel model, Campaign campaign) {
// Отправляем событие показа (impression)
// Лучше использовать VisibilityDetector для более точного отслеживания
final impressionEngagement = ContentImpressionEngagement(
decisionId: campaign.variations.first.decisionId,
content: campaign.variations.first.payload,
campaign: campaign,
);
GravitySDK.instance.sendContentEngagement(impressionEngagement);
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
model.title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
SizedBox(height: 24),
...model.buttons.map((buttonModel) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: ElevatedButton(
onPressed: () => _onPaymentButtonPressed(buttonModel, campaign),
style: buttonModel.style == 'primary' ? null : ElevatedButton.styleFrom(backgroundColor: Colors.grey),
child: Text(buttonModel.text),
),
);
}).toList(),
],
),
);
}
void _onPaymentButtonPressed(PaymentButtonModel button, Campaign campaign) {
print('Button "${button.text}" pressed!');
// Отправляем событие клика в Gravity Field
final engagement = ContentClickEngagement(
decisionId: campaign.variations.first.decisionId,
content: campaign.variations.first.payload,
campaign: campaign,
);
GravitySDK.instance.sendContentEngagement(engagement);
// Здесь ваша логика обработки платежа...
}
}
#
Шаг 4: Отслеживание взаимодействия (Engagement)
Это критически важный шаг. Чтобы A/B-тест собрал статистику, мы должны сообщить Gravity Field, когда пользователь взаимодействует с нашим кастомным UI. Для этого используется метод GravitySDK.instance.sendContentEngagement(...)
.
Дополним наш метод _onPaymentButtonPressed
:
void _onPaymentButtonPressed(PaymentButtonModel button, Campaign campaign) {
print('Button "${button.text}" pressed!');
// Отправляем событие клика в Gravity Field
final engagement = ContentClickEngagement(
decisionId: campaign.variations.first.decisionId,
content: campaign.variations.first.payload,
campaign: campaign,
);
GravitySDK.instance.sendContentEngagement(engagement);
// Здесь ваша логика обработки платежа...
}
Теперь при каждом нажатии на кнопку SDK будет отправлять событие, которое будет учтено в отчете по A/B-тесту.
#
Шаг 5: Анализ результатов
После того как кампания запущена и собрала достаточно данных, вы можете проанализировать результаты в интерфейсе Gravity Field.
Перейдите в отчет по вашей кампании и изучите метрики для каждой вариации, чтобы определить, какой из вариантов UI оказался более эффективным.
Подробнее об анализе отчетов по кампаниям.
#
FAQ
Q: Что делать, если SDK не вернул JSON?
A: Всегда предусматривайте "запасной" (fallback) UI. Ваш FutureBuilder
должен обрабатывать null или ошибочные состояния и показывать интерфейс по умолчанию. Это гарантирует, что пользовательский опыт не пострадает, если кампания неактивна или нет сети.
Q: Как обрабатывать состояние загрузки?
A: FutureBuilder
отлично справляется с этим. Пока snapshot.connectionState
равен ConnectionState.waiting
, показывайте индикатор загрузки, например, CircularProgressIndicator
.
Q: Можно ли кешировать ответ от SDK?
A: Не рекомендуется. SDK управляет логикой показа (например, частотой), и кеширование может нарушить ее. Каждый раз запрашивайте данные заново при входе на экран.
Q: В response.data.custom
приходит строка вместо Map
. Что делать?
A: Иногда JSON может приходить в виде строки. В этом случае его нужно распарсить вручную с помощью jsonDecode()
. В полном примере кода показано, как обработать оба случая.