Flutter SDK: обзор и быстрый старт

Gravity Field SDK для Flutter передаёт просмотры экранов и события, получает персонализированный контент и показывает кампании в приложении. Решения о таргетинге и выборе A/B-варианта принимает сервер. Приложение обрабатывает навигацию и может полностью управлять отрисовкой.

Руководство описывает gravity_sdk 0.24.1. Требования, указанные в пакете: Flutter ≥3.38.1 и Dart ^3.10.0. Примеры проверены анализатором и сборкой Flutter bundle на Flutter 3.38.1 / Dart 3.10.0 и Flutter 3.44.0 / Dart 3.12.0. Пакет на pub.dev · API Reference.

Задача С чего начать
Показать готовую кампанию или рекомендации Кампании и рекомендации
Отрисовать контент самостоятельно Headless и Custom JSON
Проверить варианты интерфейса A/B через JSON
Менять UI по серверной конфигурации Backend-Driven UI
Написать собственный HTTP-клиент Интеграция без SDK

Установка

Добавьте зависимости в pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  gravity_sdk: 0.24.1
  url_launcher: ^6.3.1
  webview_flutter: ^4.11.1

Выполните flutter pub get. url_launcher и webview_flutter нужны приложению из примера для открытия URL; SDK сам навигацию не выполняет.

На Android для запросов требуется разрешение android.permission.INTERNET в android/app/src/main/AndroidManifest.xml, в том числе для release-сборки:

<uses-permission android:name="android.permission.INTERNET" />

SDK читает статус App Tracking Transparency на iOS, но сам не запрашивает разрешение. Если ваше приложение запрашивает ATT, добавьте NSUserTrackingUsageDescription в ios/Runner/Info.plist и описание использования данных. Отказ в ATT не отменяет обычную интеграцию SDK. Статус push передаётся отдельно.

Подготовка кампании

  1. Получите apiKey и section для вашего проекта.
  2. Для inline-рекомендаций создайте и опубликуйте мобильную кампанию с selector product_recommendations.
  3. Настройте контекст товара: PRODUCT, SKU sku-123 и location app://product/sku-123. Для другого товара замените SKU и в кампании, и в примере.
  4. Для проверки автопоказа создайте in-app кампанию с активацией по просмотру этого экрана либо по событию add-to-cart-v1.
  5. Для проверки навигации используйте HTTPS-ссылку или диплинк app://product/sku-123.

SKU, категории и lng должны совпадать с фидом, включая регистр. Настройка в кабинете описана в разделе Кампании в приложении.

Минимальное приложение

Скопируйте quick_start.dart в lib/main.dart, замените ключ и секцию, затем выполните flutter run на iOS или Android.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:gravity_sdk/gravity_sdk.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:webview_flutter/webview_flutter.dart';

final navigatorKey = GlobalKey<NavigatorState>();

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await GravitySDK.instance.initialize(
    apiKey: 'YOUR_API_KEY',
    section: 'YOUR_SECTION_ID',
    gravityEventCallback: (event) {
      if (event is FollowUrlEvent) {
        unawaited(openCampaignUrl(event));
      } else if (event is FollowDeeplinkEvent) {
        // В этом примере поддерживается маршрут app://product/<sku>.
        final uri = Uri.tryParse(event.deeplink);
        final navigator = navigatorKey.currentState;
        if (uri != null &&
            uri.scheme == 'app' &&
            uri.host == 'product' &&
            uri.pathSegments.isNotEmpty &&
            navigator != null) {
          unawaited(
            navigator.push<void>(
              MaterialPageRoute(
                builder: (_) => ProductPage(productId: uri.pathSegments.first),
              ),
            ),
          );
        }
      }
    },
  );
  runApp(const DemoApp());
}

Future<void> openCampaignUrl(FollowUrlEvent event) async {
  final uri = Uri.tryParse(event.url);
  if (uri == null || !['https', 'http'].contains(uri.scheme)) return;
  try {
    if (event.type == FollowUrlType.webview) {
      final navigator = navigatorKey.currentState;
      if (navigator == null) return;
      await navigator.push<void>(
        MaterialPageRoute(builder: (_) => CampaignWebView(url: uri)),
      );
    } else {
      final opened = await launchUrl(uri, mode: LaunchMode.externalApplication);
      if (!opened) debugPrint('Не удалось открыть ссылку кампании');
    }
  } catch (error) {
    debugPrint('Ошибка открытия ссылки: $error');
  }
}

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    navigatorKey: navigatorKey,
    home: const ProductPage(productId: 'sku-123'),
  );
}

class ProductPage extends StatefulWidget {
  const ProductPage({super.key, required this.productId});
  final String productId;

  @override
  State<ProductPage> createState() => _ProductPageState();
}

class _ProductPageState extends State<ProductPage> {
  PageContext get pageContext => PageContext(
    type: ContextType.product,
    data: [widget.productId],
    location: 'app://product/${widget.productId}',
  );

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) return;
      unawaited(
        GravitySDK.instance.trackView(
          context: context,
          pageContext: pageContext,
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Товар')),
    body: ListView(
      padding: const EdgeInsets.all(16),
      children: [
        Text(widget.productId),
        const Text('Цена: 1990 ₽'),
        FilledButton(
          onPressed: () async {
            // В реальном приложении событие отправляют после
            // успешного изменения корзины.
            await GravitySDK.instance.triggerEvent(
              context: context,
              events: [
                AddToCartEvent(
                  value: 1990,
                  productId: widget.productId,
                  quantity: 1,
                  currency: 'RUB',
                ),
              ],
              pageContext: pageContext,
            );
          },
          child: const Text('Отправить AddToCartEvent'),
        ),
        GravityInlineWidget(
          key: ValueKey(widget.productId),
          selector: 'product_recommendations',
          height: 280,
          pageContext: pageContext,
        ),
      ],
    ),
  );
}

class CampaignWebView extends StatefulWidget {
  const CampaignWebView({super.key, required this.url});
  final Uri url;

  @override
  State<CampaignWebView> createState() => _CampaignWebViewState();
}

class _CampaignWebViewState extends State<CampaignWebView> {
  late final WebViewController controller;

  @override
  void initState() {
    super.initState();
    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.disabled)
      ..loadRequest(widget.url);
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('Кампания')),
    body: WebViewWidget(controller: controller),
  );
}

Пример отправляет просмотр после первого кадра, показывает inline-блок и позволяет отправить событие добавления товара в корзину. Кнопка демонстрирует трекинг; в вашем приложении AddToCartEvent должен отправляться после успешного изменения корзины.

Встроенный WebView в примере не включает JavaScript. Если он нужен странице кампании, настройте WebView по требованиям вашего приложения. Для собственных диплинков расширьте обработчик маршрутов. SDK уже отправляет tracking для действий встроенного контента: не дублируйте его в callback.

Конфигурация и контекст

initialize() вызывают перед использованием SDK. Настройки запросов, логирование и callback разобраны в Конфигурации, параметры экранов и события — в Контексте и событиях.

Идентификация пользователя

После авторизации передайте LoginEvent с согласованным CUID. Рекомендуемый cuidType — phone_hash. Различия между CUID, серверным UID и setUser() описаны в Идентификации.

resetUser()

await GravitySDK.instance.resetUser() сбрасывает пользователя и сессию, но не очищает офлайн-очередь. Выход и смена аккаунта.

GravityInlineWidget: автоматический рендеринг

Для размещения кампании в вёрстке используйте GravityInlineWidget с selector и PageContext. Параметры и ограничения.

GravityAnchor и tooltip-кампании

GravityAnchor регистрирует якорь; onReady сообщает, что элемент готов к показу подсказки. Пример tooltip.

Правила рекомендаций (rules)

rules передаются в запросы контента и GravityInlineWidget. Названия полей и операций согласуйте с конфигурацией проекта. Пример правила.

Следующие шаги