# Конфигурация Flutter SDK

Все примеры рассчитаны на `gravity_sdk 0.24.0`. Импорт:

```dart
import 'package:gravity_sdk/gravity_sdk.dart';
```

## Инициализация

До `runApp()` вызовите `WidgetsFlutterBinding.ensureInitialized()`, затем дождитесь `initialize()`. Инициализация настраивает клиент и запускает очередь доставки; серверный UID появляется после первого успешного запроса, а не просто после `initialize()`.

```dart
await GravitySDK.instance.initialize(
  apiKey: 'YOUR_API_KEY',
  section: 'YOUR_SECTION_ID',
  logLevel: LogLevel.info,
  gravityEventCallback: (event) {
    // Диагностика; обработку навигации добавьте по быстрому старту.
    print(event.runtimeType);
  },
);
```

| Параметр | Назначение |
|---|---|
| `apiKey`, `section` | Обязательные настройки проекта |
| `productWidgetBuilder` | Собственная карточка товара вместо стандартной |
| `gravityEventCallback` | События загрузки, показа и действия пользователя |
| `gravityContentCallback` | Ответ `GravityDataResponse<ContentResponse>` от headless-методов |
| `logLevel` | По умолчанию `LogLevel.info` |

Перед вызовами трекинга и получения контента SDK должен быть инициализирован. Исключения для работы до инициализации: API серверного UID и presentation lock, перечисленные в [справочнике](./api_reference.md).

## Глобальные настройки

```dart
GravitySDK.instance.setOptions(
  options: const Options(
    isReturnUserInfo: true,
  ),
  contentSettings: const ContentSettings(
    skusOnly: false,
    fields: ['name', 'price', 'imageUrl'],
  ),
  isFetchContentOnTrack: true,
  offlineQueue: const OfflineQueueSettings(
    enabled: true,
    maxEntries: 500,
    maxAge: Duration(days: 7),
  ),
  staleContentTimeout: const Duration(seconds: 10),
);
```

`setOptions()` заменяет только явно переданные настройки. Объекты `Options` и `ContentSettings` заменяются целиком, а не объединяются с предыдущими. Пропущенные параметры сохраняют прежние значения. Через `proxyUrl` можно задать базовый URL прокси, обслуживающего SDK endpoints; `null` не сбрасывает ранее заданный URL.

### Options

| Поле | По умолчанию | Назначение |
|---|---|---|
| `isReturnCounter` | `false` | Запросить счётчики в ответе |
| `isReturnUserInfo` | `false` | Запросить информацию о пользователе |
| `isReturnAnalyticsMetadata` | `false` | Запросить аналитические метаданные |
| `isImplicitPageview` | `false` | Серверная опция неявного просмотра |
| `isImplicitImpression` | `true` | Серверная опция неявного impression |
| `isBuildEngagementUrl` | `true` | SDK всегда запрашивает URL для engagement; параметра конструктора нет |

Изменения серверных опций аналитики согласуйте с моделью учёта проекта. Они не заменяют обработку фактической видимости собственного UI, описанную в [headless-гайде](./flutter_headless_guide.md#аналитика-собственного-ui).

`ContentSettings.skusOnly` по умолчанию `false`; `fields` — список атрибутов товара из фида либо `null`. Доступные имена полей определяет ваш фид.

### isFetchContentOnTrack

В версии 0.24.0 этот флаг проверяют **только** `trackViewNoShow()` и `triggerEventNoShow()`. При `false` они отправляют просмотр или событие и возвращают `null` без последующего получения контента.

Флаг не отключает автопоказ обычных `trackView()` и `triggerEvent()`. Для временной блокировки такого показа используйте presentation lock.

### staleContentTimeout

По умолчанию — **10 секунд**. Ответ `/visit` или `/event`, пришедший позже порога, не запускает загрузку контента. В обычном автопоказе SDK также проверяет актуальность перед отображением после загрузки и задержки кампании; её настроенная задержка учитывается отдельно.

Этот параметр ограничивает бюджет повторных попыток `/visit`, `/event` и `/choose`, но не является жёстким сетевым таймаутом и не гарантирует отмену запроса при смене экрана. В собственной отрисовке проверяйте `mounted` и актуальность запроса самостоятельно.

## Логирование

Уровни: `none`, `error`, `warn`, `info`, `debug`. Для диагностики используйте `LogLevel.debug` при инициализации. Он включает тела запросов и ответов; для обычной работы выбирайте нужную приложению детализацию.

## Callback и действия

| Событие | Ответственность приложения |
|---|---|
| `FollowUrlEvent` | Открыть `event.url` в браузере или WebView согласно `event.type` |
| `FollowDeeplinkEvent` | Обработать `event.deeplink` своим роутером |
| `RequestPushEvent` | Запросить разрешение через используемый push-пакет, обновить статус SDK |
| `CopyEvent` | SDK уже скопировал значение; callback можно использовать для реакции UI |
| События загрузки и показа | Наблюдать за работой SDK, не дублировать engagement |

Рабочая навигация приведена в [быстром старте](./ask_flutter.md#минимальное-приложение). Для собственных бизнес-событий используйте `CustomEvent`; повторно отправлять tracking уже обработанной SDK кнопки не нужно.

`gravityContentCallback` задаётся в `initialize()`. Его вызывают успешные `getContentBySelectorWithDetails()` и `getContentByCampaignIdWithDetails()`, в том числе при получении контента через `NoShow`. Выберите один путь обновления UI — callback или возвращённое значение — чтобы не обрабатывать один ответ дважды.

## Push-уведомления

```dart
GravitySDK.instance.setNotificationPermissionStatus(
  NotificationPermissionStatus.granted,
);
```

Возможные значения: `granted`, `denied`, `unknown`; по умолчанию `unknown`. SDK передаёт статус серверу, но не запрашивает разрешение и не заменяет push-провайдер приложения. Обновляйте статус после проверки или изменения разрешения ОС.

## Блокировка автопоказа

Например, на время оплаты:

```dart
GravitySDK.instance.lockPresentation();
try {
  await showDialog<void>(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Оплата'),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Закрыть'),
        ),
      ],
    ),
  );
} finally {
  GravitySDK.instance.unlockPresentation();
}
```

Этот фрагмент используется внутри Flutter-экрана с импортом `package:flutter/material.dart`.

Блокировка действует на автопоказ из `trackView()` и `triggerEvent()`. Загрузка контента и contentLoaded продолжаются: запрос выбора может расходовать серверные лимиты частоты даже без показа. Уже открытая кампания не закрывается.

Блокировка не распространяется на `GravityAnchor`/`fetchAnchorContent()`, переходы между шагами открытой кампании и ручную отрисовку. Пропущенный контент не появляется после `unlockPresentation()`. Состояние хранится в памяти и сбрасывается при перезапуске; блокировка — один boolean, без счётчика вложенных операций.

`isPresentationLocked` возвращает состояние, `setPresentationLockListener()` устанавливает один listener. Он вызывается при каждом вызове lock/unlock, включая повторные; `null` снимает подписку.
