# Гайд: Backend-Driven UI (BDUI) с Flutter SDK

Этот гайд представляет собой пошаговое руководство по использованию `gravity-sdk-flutter` для реализации паттерна Backend-Driven UI (BDUI). BDUI — это мощный подход, который позволяет динамически изменять интерфейс вашего приложения с сервера, без необходимости выпускать новые версии в App Store или Google Play.

Пользовательская настройка кампаний и доступные in-app форматы описаны в разделе [Кампании в приложении](/personalization/Campaigns/app_campaigns/getting_started.md).

Мы рассмотрим практический бизнес-сценарий: динамическое изменение кнопки на карточке товара в зависимости от сегмента пользователя. Это поможет проиллюстрировать весь процесс — от настройки кампании в Gravity Field до реализации во Flutter, включая обработку действий пользователя и отслеживание аналитики.

**Ключевые цели этого гайда:**
1.  **Объяснить концепцию:** Понять, что такое BDUI и какие преимущества он дает.
2.  **Предоставить практические шаги:** Создать пошаговое руководство для реализации динамического компонента.
3.  **Продвигать лучшие практики:** Сделать акцент на надежной реализации, включая моделирование данных, управление состоянием, обработку ошибок и аналитику.

---

## Что такое 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-кампании

1.  Перейдите в раздел **Campaigns → API Campaigns** и нажмите **Создать кампанию**.
2.  Выберите тип кампании **Custom JSON**.
3.  Задайте **API-селектор**. Это уникальный идентификатор, по которому ваше Flutter-приложение будет запрашивать эту конфигурацию. Назовем его `product_card_main_button`.

### 1.2. Настройка вариаций JSON

Теперь создадим две вариации, по одной для каждого сегмента пользователей.

#### Вариация A: Для новых пользователей

1.  Создайте первую вариацию (например, назовите ее "Добавить в избранное").
2.  В поле JSON вставьте следующую конфигурацию:

```json
{
  "type": "FAVORITE_BUTTON",
  "text": "Добавить в избранное",
  "icon": "heart_outline",
  "action": {
    "type": "ADD_TO_FAVORITES",
    "productId": "12345"
  }
}
```
-   `type`, `text`, `icon`: Описывают внешний вид кнопки.
-   `action`: Описывает действие, которое должно произойти при нажатии. Мы передаем тип действия (`ADD_TO_FAVORITES`) и `productId`, чтобы приложение знало, какой товар добавить в избранное.

#### Вариация B: Для вернувшихся пользователей

1.  Создайте вторую вариацию (например, "Оценить товар").
2.  В поле JSON вставьте следующую конфигурацию:

```json
{
  "type": "RATE_BUTTON",
  "text": "Оценить товар",
  "icon": "star_outline",
  "action": {
    "type": "SHOW_RATING_DIALOG",
    "productId": "12345"
  }
}
```
-   Здесь `action.type` — `SHOW_RATING_DIALOG`, что говорит приложению открыть диалог оценки для указанного `productId`.

### 1.3. Настройка таргетинга

Теперь свяжем каждую вариацию с нужным сегментом пользователей.

1.  В рамках кампании создайте два сценария (Experiences).
2.  **Сценарий для новых пользователей:**
    -   В настройках таргетинга выберите аудиторию `New users`.
    -   Внутри этого сценария оставьте только вариацию "Добавить в избранное".
3.  **Сценарий для вернувшихся пользователей:**
    -   В настройках таргетинга выберите аудиторию `Returning users`.
    -   Внутри этого сценария оставьте только вариацию "Оценить товар".

После сохранения и публикации кампании наш "бэкенд" готов отдавать нужную конфигурацию UI.

---

## Часть 2: Реализация во Flutter

Теперь перейдем к Flutter-приложению. Нам нужно получить JSON, распарсить его в строго типизированные модели и на основе этих данных построить виджет.

### 2.1. Получение данных

Для получения конфигурации используем метод `GravitySDK.instance.getContentBySelector`.

```dart
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-классы — это лучшая практика. Она защищает от ошибок во время выполнения и делает код более читаемым.

```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.

```dart
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)

Создадим централизованный обработчик, который будет вызывать нужную бизнес-логику и отправлять событие клика.

```dart
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-элементов можно реализовать стратегию кэширования конфигурации, чтобы уменьшить задержки при повторном открытии экрана.

Этот подход открывает огромные возможности для экспериментов и быстрой адаптации вашего приложения под нужды бизнеса и пользователей.
