#
Гайд: Backend-Driven UI (BDUI) с Flutter SDK
Этот гайд представляет собой пошаговое руководство по использованию gravity-sdk-flutter
для реализации паттерна Backend-Driven UI (BDUI). BDUI — это мощный подход, который позволяет динамически изменять интерфейс вашего приложения с сервера, без необходимости выпускать новые версии в App Store или Google Play.
Мы рассмотрим практический бизнес-сценарий: динамическое изменение кнопки на карточке товара в зависимости от сегмента пользователя. Это поможет проиллюстрировать весь процесс — от настройки кампании в Gravity Field до реализации во Flutter, включая обработку действий пользователя и отслеживание аналитики.
Ключевые цели этого гайда:
- Объяснить концепцию: Понять, что такое BDUI и какие преимущества он дает.
- Предоставить практические шаги: Создать пошаговое руководство для реализации динамического компонента.
- Продвигать лучшие практики: Сделать акцент на надежной реализации, включая моделирование данных, управление состоянием, обработку ошибок и аналитику.
#
Что такое BDUI и зачем его использовать с Gravity Field?
Backend-Driven UI (BDUI) — это архитектурный паттерн, при котором сервер отправляет в мобильное приложение не просто данные, а описание того, как должен выглядеть и вести себя пользовательский интерфейс. Приложение, в свою очередь, "рисует" этот интерфейс на основе полученных инструкций.
Преимущества использования BDUI с Gravity Field:
- Гибкость: Меняйте UI "на лету". Запускайте акции, меняйте тексты, кнопки и даже целые блоки без релиза новой версии приложения.
- A/B-тестирование UI: Легко тестируйте разные варианты интерфейса на разных сегментах пользователей и измеряйте, какой из них работает лучше.
- Персонализация: Показывайте разным пользователям разный интерфейс. Например, новичкам — подсказки, а постоянным клиентам — специальные предложения.
- Централизованное управление: Вся логика отображения UI находится в одном месте — в интерфейсе Gravity Field, что упрощает управление и снижает количество ошибок.
#
Описание сценария: Динамическая кнопка на карточке товара
Представим, что мы хотим повысить вовлеченность пользователей на карточке товара. Наша гипотеза заключается в том, что разным сегментам пользователей нужны разные призывы к действию.
- Для новых пользователей: Мы хотим, чтобы они добавляли товары в избранное, формируя свой список желаний. Для них основной кнопкой будет "Добавить в избранное".
- Для вернувшихся пользователей: Мы хотим собрать обратную связь о товарах, которые они, возможно, уже покупали. Для них основной кнопкой будет "Оценить товар".
С помощью BDUI мы реализуем эту логику так, чтобы приложение само запрашивало конфигурацию кнопки у Gravity Field и отображало нужный вариант в зависимости от сегмента, к которому относится текущий пользователь.
#
Часть 1: Настройка в Gravity Field
Первый шаг — создать "бэкенд" конфигурацию для нашего UI в интерфейсе Gravity Field. Мы создадим API-кампанию, которая будет возвращать JSON с описанием кнопки.
#
1.1. Создание API-кампании
- Перейдите в раздел Campaigns → API Campaigns и нажмите Создать кампанию.
- Выберите тип кампании Custom JSON.
- Задайте API-селектор. Это уникальный идентификатор, по которому ваше Flutter-приложение будет запрашивать эту конфигурацию. Назовем его
product_card_main_button
.
#
1.2. Настройка вариаций JSON
Теперь создадим две вариации, по одной для каждого сегмента пользователей.
#
Вариация A: Для новых пользователей
- Создайте первую вариацию (например, назовите ее "Добавить в избранное").
- В поле JSON вставьте следующую конфигурацию:
{
"type": "FAVORITE_BUTTON",
"text": "Добавить в избранное",
"icon": "heart_outline",
"action": {
"type": "ADD_TO_FAVORITES",
"productId": "12345"
}
}
type
,text
,icon
: Описывают внешний вид кнопки.action
: Описывает действие, которое должно произойти при нажатии. Мы передаем тип действия (ADD_TO_FAVORITES
) иproductId
, чтобы приложение знало, какой товар добавить в избранное.
#
Вариация B: Для вернувшихся пользователей
- Создайте вторую вариацию (например, "Оценить товар").
- В поле JSON вставьте следующую конфигурацию:
{
"type": "RATE_BUTTON",
"text": "Оценить товар",
"icon": "star_outline",
"action": {
"type": "SHOW_RATING_DIALOG",
"productId": "12345"
}
}
- Здесь
action.type
—SHOW_RATING_DIALOG
, что говорит приложению открыть диалог оценки для указанногоproductId
.
#
1.3. Настройка таргетинга
Теперь свяжем каждую вариацию с нужным сегментом пользователей.
- В рамках кампании создайте два сценария (Experiences).
- Сценарий для новых пользователей:
- В настройках таргетинга выберите аудиторию
New users
. - Внутри этого сценария оставьте только вариацию "Добавить в избранное".
- В настройках таргетинга выберите аудиторию
- Сценарий для вернувшихся пользователей:
- В настройках таргетинга выберите аудиторию
Returning users
. - Внутри этого сценария оставьте только вариацию "Оценить товар".
- В настройках таргетинга выберите аудиторию
После сохранения и публикации кампании наш "бэкенд" готов отдавать нужную конфигурацию UI.
#
Часть 2: Реализация во Flutter
Теперь перейдем к Flutter-приложению. Нам нужно получить JSON, распарсить его в строго типизированные модели и на основе этих данных построить виджет.
#
2.1. Получение данных
Для получения конфигурации используем метод GravitySDK.instance.getContentBySelector
.
import 'package:gravity_sdk/gravity_sdk.dart';
import 'dart:convert';
// Вспомогательный класс для возврата данных
class ButtonConfigResponse {
final ButtonConfig config;
final Campaign campaign;
ButtonConfigResponse({required this.config, required this.campaign});
}
Future<ButtonConfigResponse?> fetchButtonConfig(String productId) async {
try {
final response = await GravitySDK.instance.getContentBySelector(
selector: 'product_card_main_button',
pageContext: PageContext(
type: ContextType.product,
data: [productId], // Передаем ID текущего товара
location: 'app://product/$productId',
),
);
if (response.data.isNotEmpty) {
final campaign = response.data.first;
final content = campaign.payload.first.contents.first;
// Для Custom JSON кампаний, контент может быть в поле `variables`
// или в другом поле в зависимости от конфигурации.
// Здесь мы предполагаем, что `variables` содержит наш JSON.
// В реальном SDK может потребоваться другой способ доступа.
final customData = (content.variables as dynamic).customData;
Map<String, dynamic> configJson;
if (customData is String) {
configJson = jsonDecode(customData) as Map<String, dynamic>;
} else {
configJson = customData as Map<String, dynamic>;
}
final buttonConfig = ButtonConfig.fromJson(configJson);
return ButtonConfigResponse(config: buttonConfig, campaign: campaign);
}
return null;
} catch (e) {
// Обработка ошибок (например, нет сети)
print('Error fetching button config: $e');
return null;
}
}
#
2.2. Моделирование данных (Data Models)
Парсинг JSON в строго типизированные Dart-классы — это лучшая практика. Она защищает от ошибок во время выполнения и делает код более читаемым.
// Enum для типов действий для повышения надежности кода
enum ActionType {
addToFavorites,
showRatingDialog,
unknown
}
// Модель для описания действия, которое нужно выполнить
class ButtonAction {
final ActionType type;
final String productId;
ButtonAction({required this.type, required this.productId});
factory ButtonAction.fromJson(Map<String, dynamic> json) {
ActionType actionType;
switch (json['type'] as String?) {
case 'ADD_TO_FAVORITES':
actionType = ActionType.addToFavorites;
break;
case 'SHOW_RATING_DIALOG':
actionType = ActionType.showRatingDialog;
break;
default:
actionType = ActionType.unknown;
}
return ButtonAction(
type: actionType,
productId: json['productId'] as String,
);
}
}
// Основная модель конфигурации кнопки
class ButtonConfig {
final String type;
final String text;
final String icon;
final ButtonAction action;
ButtonConfig({
required this.type,
required this.text,
required this.icon,
required this.action,
});
factory ButtonConfig.fromJson(Map<String, dynamic> json) {
return ButtonConfig(
type: json['type'] as String,
text: json['text'] as String,
icon: json['icon'] as String,
action: ButtonAction.fromJson(json['action'] as Map<String, dynamic>),
);
}
}
#
2.3. Создание динамического виджета
Используем FutureBuilder
для асинхронной загрузки конфигурации и отображения UI.
import 'package:flutter/material.dart';
import 'package:visibility_detector/visibility_detector.dart';
// ... импорты ваших моделей и сервисов
class DynamicProductButton extends StatefulWidget {
final String productId;
const DynamicProductButton({Key? key, required this.productId}) : super(key: key);
@override
_DynamicProductButtonState createState() => _DynamicProductButtonState();
}
class _DynamicProductButtonState extends State<DynamicProductButton> {
late Future<ButtonConfigResponse?> _configFuture;
@override
void initState() {
super.initState();
_configFuture = fetchButtonConfig(widget.productId);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<ButtonConfigResponse?>(
future: _configFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Состояние загрузки: показываем placeholder (например, Shimmer)
return const CircularProgressIndicator();
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data == null) {
// Ошибка или нет данных: показываем UI по умолчанию (fallback)
return _buildFallbackButton();
}
// Успех: строим кнопку на основе полученной конфигурации
final response = snapshot.data!;
return _buildDynamicUi(response.config, response.campaign);
},
);
}
Widget _buildDynamicUi(ButtonConfig config, Campaign campaign) {
final button = _buildButtonFromConfig(config, campaign);
return VisibilityDetector(
key: Key(campaign.payload.first.contents.first.contentId),
onVisibilityChanged: (visibilityInfo) {
if (visibilityInfo.visibleFraction >= 0.5) {
GravitySDK.instance.sendContentEngagement(
ContentVisibleImpressionEngagement(
campaign.payload.first.contents.first,
campaign,
),
);
}
},
child: button,
);
}
Widget _buildButtonFromConfig(ButtonConfig config, Campaign campaign) {
IconData iconData;
switch (config.icon) {
case 'heart_outline':
iconData = Icons.favorite_border;
break;
case 'star_outline':
iconData = Icons.star_border;
break;
default:
iconData = Icons.help_outline;
}
return ElevatedButton.icon(
icon: Icon(iconData),
label: Text(config.text),
onPressed: () {
_handleAction(config.action, campaign);
},
);
}
Widget _buildFallbackButton() {
return ElevatedButton.icon(
icon: const Icon(Icons.shopping_cart_outlined),
label: const Text('Добавить в корзину'),
onPressed: () { /* Логика добавления в корзину по умолчанию */ },
);
}
void _handleAction(ButtonAction action, Campaign campaign) {
final currentAction = ButtonAction(type: action.type, productId: widget.productId);
ActionHandler.handle(context, currentAction, campaign);
}
}
#
2.4. Логика обработки действий (Action Handler)
Создадим централизованный обработчик, который будет вызывать нужную бизнес-логику и отправлять событие клика.
class ActionHandler {
static void handle(BuildContext context, ButtonAction action, Campaign campaign) {
// Отправляем событие клика в Gravity Field
GravitySDK.instance.sendContentEngagement(
ContentClickEngagement(
campaign.payload.first.contents.first,
campaign,
),
);
switch (action.type) {
case ActionType.addToFavorites:
print('Добавляем товар ${action.productId} в избранное');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Товар ${action.productId} добавлен в избранное!')),
);
break;
case ActionType.showRatingDialog:
print('Показываем диалог оценки для товара ${action.productId}');
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('Оцените товар'),
content: Text('Как вам товар ${action.productId}?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context), child: Text('Закрыть')),
],
),
);
break;
case ActionType.unknown:
print('Неизвестное действие');
break;
}
}
}
#
Заключение и лучшие практики
Мы рассмотрели полный цикл реализации Backend-Driven UI с помощью Gravity Field и Flutter SDK.
Ключевые выводы и лучшие практики:
- Моделируйте данные: Всегда парсите JSON в строго типизированные Dart-классы.
- Предусматривайте Fallback UI: Ваше приложение должно оставаться функциональным, даже если сервер не вернул конфигурацию.
- Используйте Placeholder'ы: Показывайте индикаторы загрузки (скелетоны, шиммеры), чтобы улучшить пользовательский опыт при медленной сети.
- Централизуйте обработку действий: Создайте единый
ActionHandler
для обработки всех действий, приходящих с бэкенда. - Не забывайте про аналитику: Отслеживайте показы и клики, чтобы измерять эффективность ваших A/B-тестов и персонализаций.
- Рассмотрите кэширование: Для некритичных UI-элементов можно реализовать стратегию кэширования конфигурации, чтобы уменьшить задержки при повторном открытии экрана.
Этот подход открывает огромные возможности для экспериментов и быстрой адаптации вашего приложения под нужды бизнеса и пользователей.