A/B-тестирование интерфейса через JSON
В этом примере Gravity Field выбирает текст кнопки оформления заказа, а Flutter-приложение рисует её своим виджетом. Вариант назначает сервер; приложение не выбирает A/B локально.
Требуется gravity_sdk 0.24.0 и выполненная инициализация. Общий контракт получения данных и аналитики — в headless-гайде.
Настройка кампании
- Создайте кампанию с выдачей Custom JSON/API и selector
checkout_button. - Настройте аудиторию и контекст корзины:
CART, locationapp://cart. - Создайте две вариации и распределение трафика для A/B-теста.
- Настройте данные так, чтобы в ответе
content.variables['checkout_button']возвращался объект одной из схем ниже. - Настройте tracking показа, видимости и клика; для ручного клика в ответе требуется
eventsс типомclick. - Опубликуйте кампанию и проверьте фактический ответ.
Вариация A:
{"variant": "A", "label": "Оформить заказ"}
Вариация B:
{"variant": "B", "label": "Перейти к оплате"}
Это содержимое ключа checkout_button, а не полный ответ SDK. SDK не навязывает формат пользовательских переменных.
Подключение во Flutter
Добавьте visibility_detector: ^0.4.0+2 в pubspec.yaml и выполните flutter pub get. Скопируйте gravity_content.dart и ab_example.dart рядом в lib/.
import 'package:flutter/material.dart';
import 'package:gravity_sdk/gravity_sdk.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'gravity_content.dart';
class CheckoutVariantButton extends StatefulWidget {
const CheckoutVariantButton({super.key, required this.onCheckout});
final VoidCallback onCheckout;
@override
State<CheckoutVariantButton> createState() => _CheckoutVariantButtonState();
}
class _CheckoutVariantButtonState extends State<CheckoutVariantButton> {
ContentExposure? exposure;
String label = 'Оформить заказ';
Key visibilityKey = UniqueKey();
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final selected = await loadSelectedContent(
selector: 'checkout_button',
pageContext: const PageContext(
type: ContextType.cart,
data: [],
location: 'app://cart',
),
);
if (!mounted || selected == null) return;
final config = selected.content.variables.valueOf<Map<String, dynamic>>(
'checkout_button',
);
if (config == null) return;
final variant = config['variant'];
final text = config['label'];
if ((variant != 'A' && variant != 'B') ||
text is! String ||
text.trim().isEmpty) {
return;
}
setState(() {
label = text;
exposure = ContentExposure(selected);
visibilityKey = UniqueKey();
});
} catch (_) {
// Кнопка оформления остаётся доступна с исходным текстом.
}
}
@override
Widget build(BuildContext context) => VisibilityDetector(
key: visibilityKey,
onVisibilityChanged: (info) => exposure?.onVisibility(info.visibleFraction),
child: FilledButton(
onPressed: () {
exposure?.click();
widget.onCheckout();
},
child: Text(label),
),
);
}
Подключите к экрану корзины:
import 'ab_example.dart';
// В дереве экрана корзины:
CheckoutVariantButton(
onCheckout: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const Scaffold(
body: Center(child: Text('Оформление заказа')),
),
),
);
},
)
В вашем приложении замените демонстрационный маршрут на экран оформления. Отправляйте PurchaseEvent после успешной покупки, а не при нажатии на кнопку. Пример покупки.
Как учитываются показы и клики
До ответа показывается обычная рабочая кнопка. Если пришла поддерживаемая конфигурация, приложение применяет вариант, создаёт ContentExposure и новый ключ detector. Показ фиксируется только после появления выбранной кнопки в UI.
При прокрутке и перестроении один экземпляр ContentExposure не отправляет повторные impression/visible impression. Каждый реальный клик учитывается отдельно. Fallback без принятой конфигурации не считается показом серверного варианта.
onCheckout не блокируется сетевым запросом и работает даже при ошибке или пустом ответе. При смене пользователя пересоздайте виджет с новым Key; не оставляйте выбранный вариант предыдущего аккаунта.
Проверка эксперимента
- Обе схемы возвращаются корректно, надпись соответствует принятому ответу.
- Пустой ответ, неверный тип, неизвестный вариант или пустая надпись оставляют исходную кнопку.
- Уход с экрана до ответа не вызывает обновление размонтированного состояния.
- Повторное
build()не вызывает новую загрузку и не дублирует показ. - В отчёте появляются tracking выбранной кампании и бизнес-конверсия после реальной покупки.
- Между запросами одного пользователя не сбрасываются UID и сессия ради получения другого варианта.
Распределение и атрибуция определяются настройками эксперимента на сервере; SDK не обещает постоянный вариант при произвольной смене идентификации или конфигурации кампании.