# 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](https://pub.dev/packages/gravity_sdk/versions/0.24.1) · [API Reference](https://pub.dev/documentation/gravity_sdk/0.24.1/).

| Задача | С чего начать |
|---|---|
| Показать готовую кампанию или рекомендации | [Кампании и рекомендации](./campaigns.md) |
| Отрисовать контент самостоятельно | [Headless и Custom JSON](./flutter_headless_guide.md) |
| Проверить варианты интерфейса | [A/B через JSON](./flutter_ab_test_json_guide.md) |
| Менять UI по серверной конфигурации | [Backend-Driven UI](./flutter_bdui_guide.md) |
| Написать собственный HTTP-клиент | [Интеграция без SDK](../flutter_api_integration_guide.md) |

## Установка

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

```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-сборки:

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

SDK читает статус App Tracking Transparency на iOS, но сам не запрашивает разрешение. Если ваше приложение запрашивает ATT, добавьте `NSUserTrackingUsageDescription` в `ios/Runner/Info.plist` и описание использования данных. Отказ в ATT не отменяет обычную интеграцию SDK. Статус push передаётся [отдельно](./configuration.md#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` должны совпадать с фидом, включая регистр. Настройка в кабинете описана в разделе [Кампании в приложении](/personalization/Campaigns/app_campaigns/getting_started.md).

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

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

```dart
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 разобраны в [Конфигурации](./configuration.md), параметры экранов и события — в [Контексте и событиях](./events.md).

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

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

### resetUser()

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

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

Для размещения кампании в вёрстке используйте `GravityInlineWidget` с selector и `PageContext`. [Параметры и ограничения](./campaigns.md#inline-блок).

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

`GravityAnchor` регистрирует якорь; `onReady` сообщает, что элемент готов к показу подсказки. [Пример tooltip](./campaigns.md#tooltip).

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

`rules` передаются в запросы контента и `GravityInlineWidget`. Названия полей и операций согласуйте с конфигурацией проекта. [Пример правила](./campaigns.md#правила-рекомендаций).

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

- [Офлайн-доставка событий](./offline.md).
- [Справочник API](./api_reference.md).
- [Проверка интеграции перед выпуском](./troubleshooting.md).
