---
title: "Обработка данных из флоу в Flutter SDK"
description: "Сохраняйте и используйте данные, которые пользователи вводят во флоу, в своём Flutter-приложении с помощью Adapty SDK."
---

> **AI agents**: to search Adapty docs faster and with fewer tokens, install the Adapty skill. Claude Code (self-updating via plugin): `claude plugin marketplace add adaptyteam/adapty-skills && claude plugin install adapty-skills@adapty` — other tools: `npx skills add adaptyteam/adapty-skills --all`

Когда пользователь вводит текст в поле ввода, отвечает на вопрос викторины или переключает тумблер во [флоу](adapty-flow-builder), SDK передаёт это значение в ваше приложение через колбэк аналитики.

Чаще всего приложения используют эти данные для:

- **Регистрация пользователей на собственном бэкенде**: Возьмите email и имя, которые пользователь ввёл в онбординге, и создайте его аккаунт, когда флоу закроется.
- **Сохранение ответов и предпочтений**: Отслеживайте, что выбрал пользователь, чтобы приложение могло использовать это позже — например, [записать в его профиль Adapty](flutter-setting-user-attributes) в виде custom attributes.
- **Кастомизация будущих флоу**: Сохраните ответы на вопросы как custom attributes, затем настройте таргетинг на плейсмент позже — чтобы каждый сегмент видел свой флоу или свой пейвол внутри него.
- **Передача данных в сторонние аналитические платформы**: Пересылайте ответы в Amplitude, Mixpanel или любой другой инструмент продуктовой аналитики, который вы используете.

Поля ввода и группы выбора автоматически передают свои значения. Чтобы различать их в коде, задайте каждому полю ввода уникальный **Element ID**, а каждой группе выбора — **Group ID** в конструкторе.

## Перед началом работы \{#before-you-start\}

Вам понадобится:

- **Adapty SDK v4 или новее**: колбэки флоу отсутствуют в более ранних версиях.
- **Флоу, собранный в [Flow & Paywall Builder](adapty-flow-builder)**: только флоу передают входные значения через этот колбэк.
- **Недавно опубликованная версия флоу**: флоу передаёт входные значения только если вы опубликовали его после того, как эта функция стала доступна. Если ничего не приходит в приложение, опубликуйте новую версию флоу и попробуйте снова.

## Получение введённых значений \{#receive-input-values\}

Введённые значения передаются в тот же колбэк, что и все остальные аналитические события флоу, под именем события `flow_user_input`. Реализуйте `flowViewDidReceiveAnalyticEvent` в наблюдателе, который вы регистрируете через `AdaptyUI().setFlowsEventsObserver`:

```dart showLineNumbers title="Flutter"
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  handleFlowInput(name, params);
}
```

Колбэк `flowViewDidReceiveAnalyticEvent` передаёт все аналитические события флоу, включая [просмотры экранов](flutter-flow-screen-views).
- Параметр `name` содержит название события. Чтобы отфильтровать события пользовательского ввода, сравните `name` со значением `flow_user_input`.
- Параметр `element_type` указывает категорию элемента.
- Значение ввода хранится в разных параметрах в зависимости от типа элемента:
     - Текстовые поля, выборщики и переключатели хранят пользовательский ввод в `value`
     - Группы с возможностью выбора передают активные варианты в `item_ids` и `item_titles`

```dart showLineNumbers title="Flutter"
void handleFlowInput(String name, Map<String, dynamic> params) {
  if (name != 'flow_user_input') return;

  final elementId = params['element_id'] as String;

  // The screen the input sits on. Pair it with elementId to tell apart
  // two fields that share an Element ID on different screens.
  final screenId = params['instanceId'] as String?;

  switch (params['element_type'] as String) {
    case 'text_input':
    case 'email_input':
    case 'number_input':
    case 'phone_input':
      final text = params['value'] as String;
      break;
    case 'date_picker':
    case 'time_picker':
    case 'date_time_picker':
      // Unix time in milliseconds. Arrives as int on iOS and double on Android — read it through num.
      final date = DateTime.fromMillisecondsSinceEpoch((params['value'] as num).toInt());
      break;
    case 'single_choice':
      final optionId = (params['item_ids'] as List).cast<String>().first;
      break;
    case 'multi_choice':
      final optionIds = (params['item_ids'] as List).cast<String>();
      break;
    case 'toggle':
      final isOn = params['value'] as bool;
      break;
  }
}
```

Чтобы убедиться, что коллбэк срабатывает, взаимодействуйте с полем ввода в тестовой сборке вашего приложения. Если обработчик коллбэка не получает событие, проверьте [предварительные требования](#before-you-start). Убедитесь, что флоу был опубликован после того, как эта функция стала доступной.

## Когда приложение получает входные данные \{#when-your-app-receives-the-input\}

:::important
Значение по умолчанию или предвыбранный вариант никогда не попадают в ваше приложение через этот колбэк. Если пользователь принимает опцию, отмеченную **Set as default**, и продолжает, событие не срабатывает. Отсутствие события не означает «нет ответа» — пользователь просто оставил значение по умолчанию.
:::

**Следующие элементы вызывают это событие:**

- Текстовые, email-, числовые и телефонные поля
- Выборщики даты, времени и даты со временем
- Группы с единственным и множественным выбором, а также переключатели

**Следующие не вызывают:**

- Поля пароля, выборы продукта и переключения вкладок
- Поля ввода внутри элемента Header, который является общим для всех экранов
- Группы с выбором, у которых есть дублирующиеся или отсутствующие Element ID у опций, либо Group ID, повторно используемый на другом экране. Такая группа не отправляет ничего, а не часть ответа.

**Событие срабатывает когда:**

- Поле теряет фокус. Очищенное поле сообщает пустую строку; поле, которое пользователь ни разу не редактировал, не сообщает ничего. Плейсхолдер не является значением. Если пользователь возвращается к полю, редактирует его и снова уходит, следует второе событие.
- Пользователь закрывает выборщик после выбора нового значения. Закрытие без изменений не сообщает ничего.
- Пользователь нажимает на опцию или переключатель. Событие множественного выбора содержит список всех выбранных опций, поэтому снятие последней выбранной опции отправляет два пустых массива.

**Событие не срабатывает когда:**

- Пользователь набирает текст. Потока нажатий клавиш нет — только значение, которое поле содержит в момент потери фокуса.
- Пользователь отправляет или закрывает флоу. Значение, которое в этот момент ещё редактируется, может быть потеряно; в разделе [ограничения доставки](#delivery-and-limitations) описано, как спроектировать последний экран с учётом этого.
- Значение устанавливается без взаимодействия с пользователем. Опция, отмеченная **Set as default**, предвыбирается при открытии экрана, а действие **Set Variable** может выбрать опцию или заполнить поле ввода из другого взаимодействия. Ни то ни другое не отправляет событие; предзаполненное поле ввода сообщается только после того, как пользователь его отредактирует.

## Что вы получаете \{#what-you-receive\}

Коллбэк доставляет два типа событий. Фильтруйте по `name`, чтобы отображать только события `flow_user_input`. JSON-ответ выглядит так:

```json
{
  "name": "flow_user_input",
  "instanceId": "scr_registration",
  "isBackendEvent": false,
  "isCustomerEvent": true,
  "element_id": "email",
  "element_type": "email_input",
  "value": "jane@example.com"
}
```

| Параметр          | Описание |
|:------------------|:---------|
| `name`            | `flow_user_input` для событий ввода, `flow_screen_showed` для просмотров экрана. |
| `instanceId`      | ID экрана, на котором находится поле ввода. ID элементов уникальны в пределах экрана, но не в рамках всего флоу. Если в вашем флоу есть поля ввода на нескольких экранах, используйте `instanceId` вместе с `element_id` при фильтрации событий. |
| `element_id`      | **Element ID** [поля ввода](builder-inputs-and-forms) или **Group ID** [группы выбора](flow-selectable-elements). |
| `element_type`    | Тип элемента, отправившего событие. Определяет, в каком из параметров ниже хранится значение ввода. |
| `value`           | **Только для текстовых полей, пикеров и переключателей.** Значение ввода: строка для текстовых полей, целое число для пикеров, булево значение для переключателей. |
| `item_ids`        | **Только для групп с единственным и множественным выбором.** **Element ID** выбранных вариантов в порядке их отображения в билдере. Один элемент для группы с единственным выбором; любое количество для группы с множественным выбором. |
| `item_titles`     | **Только для групп с единственным и множественным выбором.** Заголовки вариантов, перечисленных в `item_ids`, в том же порядке. Никогда не пустой: вариант без заголовка возвращает свой ID. |
| `isCustomerEvent` | Служебный флаг, всегда `true` для этого события. Помечает события, которые SDK доставляет в ваш коллбэк. Удобен, если один обработчик пересылает все события флоу в вашу аналитику, а вы фильтруете по этому флагу, а не по `name`. |
| `isBackendEvent`  | Служебный флаг, всегда `false` для этого события. Помечает события, которые Adapty также записывает для собственной аналитики. Значение `false` подтверждает, что введённые пользователями данные поступают только в ваше приложение — Adapty их не получает и не хранит. |

Что передаёт каждый элемент:

| В билдере | `element_type` | Параметр со значением | Что содержит |
|:----------|:---------------|:----------------------|:-------------|
| Ввод **Text**, **Number**, **Phone number** | `text_input`, `number_input`, `phone_input` | `value` | Строка, введённая пользователем. Числа передаются как строки, не как числовые типы. |
| Ввод **E-mail** | `email_input` | `value` | Строка, введённая пользователем, даже если она не прошла проверку формата в билдере. Валидируйте её на своей стороне перед использованием. |
| Ввод **Password** | нет | нет | Не отправляет событий. |
| Ввод **Date** | `date_picker` | `value` | Unix-время в миллисекундах, целое число, соответствующее полуночи выбранной даты по местному времени. |
| Ввод **Time** | `time_picker` | `value` | Unix-время в миллисекундах, целое число, округлённое до минуты. |
| Ввод **Date & Time** | `date_picker` и `time_picker` | `value` | Два элемента: пикер даты и пикер времени. Каждый отправляет собственное событие. |
| Ввод с переключением на **Date & Time** в выпадающем списке **Type** | `date_time_picker` | `value` | Unix-время в миллисекундах, целое число, округлённое до минуты. |
| Группа **Single choice** | `single_choice` | `item_ids`, `item_titles` | Два массива. `item_ids`: массив с Element ID выбранного варианта. `item_titles`: массив с заголовком этого варианта. |
| Группа **Multi-choice** | `multi_choice` | `item_ids`, `item_titles` | Два массива. `item_ids`: Element ID всех выбранных вариантов в порядке их отображения в билдере. `item_titles`: их заголовки в том же порядке. Оба массива пусты, если ничего не выбрано. |
| Группа **Toggle** | `toggle` | `value` | Булево значение. |

Для ветвления по ответу сравнивайте `item_ids`, а не `item_titles`. Заголовок — производная величина: **Element Title** варианта, если он задан; иначе его текст в вашей локали по умолчанию; иначе его Element ID. Пользователь, просматривавший флоу на другом языке, видел другой текст.

## Примеры событий \{#event-examples\}

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

<Details>
<summary>Текстовый, email, числовой ввод и ввод телефона (нажмите для раскрытия)</summary>

```dart
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  name;                      // 'flow_user_input'
  params['name'];            // 'flow_user_input'
  params['instanceId'];      // 'scr_J260KU5q'
  params['isCustomerEvent']; // true
  params['isBackendEvent'];  // false
  params['element_id'];      // 'email'
  params['element_type'];    // 'email_input'
  params['value'];           // 'jane@example.com'   (String)
}
```
</Details>

<Details>
<summary>Выбор даты, времени и даты-времени (нажмите, чтобы развернуть)</summary>

```dart
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  params['element_id'];      // 'birthday'
  params['element_type'];    // 'date_picker'
  params['value'];           // 645408000000   (Unix milliseconds — 1990-06-15, local midnight; int on iOS, double on Android)
}
```
</Details>

<Details>
<summary>Single choice (Click to expand)</summary>

```dart
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  params['element_id'];      // 'experience'
  params['element_type'];    // 'single_choice'
  params['item_ids'];        // ['pro']   (List)
  params['item_titles'];     // ['I train professionally']
}
```
</Details>

<Details>
<summary>Multi choice (Click to expand)</summary>

```dart
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  params['element_id'];      // 'interests'
  params['element_type'];    // 'multi_choice'
  params['item_ids'];        // ['sports', 'music']   (List)
  params['item_titles'];     // ['Sports', 'Music']
}
```
</Details>

<Details>
<summary>Toggle (Click to expand)</summary>

```dart
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  params['element_id'];      // 'reminders'
  params['element_type'];    // 'toggle'
  params['value'];           // true   (bool)
}
```
</Details>

## Доставка и ограничения \{#delivery-and-limitations\}

:::warning
Флоу передают необработанные значения — адреса электронной почты, номера телефонов и всё, что вводит пользователь. Относитесь ко всему, что приходит в аналитический колбэк, как к персональным данным, и не записывайте это туда, куда бы вы не записали email пользователя.
:::

- **Побеждает последнее значение**: вы получаете одно событие на поле с тем значением, на котором пользователь остановился, а не поток каждого нажатия клавиши. Если он редактирует поле, до вас дойдёт только последняя версия.
- **Нет гарантии отправки**: значения поступают к вам по мере того, как пользователь движется по флоу, и он может выйти в любой момент. Дождитесь закрытия флоу, прежде чем считать набор ответов завершённым.
- **Доставка по возможности**: если пользователь закрывает флоу или сворачивает приложение, пока поле ещё в фокусе или пикер открыт, это значение может быть потеряно.

Чтобы значение последнего поля было надёжным, завершайте флоу экраном без полей ввода и пикеров, и закрывайте флоу явным действием пользователя, а не автоматически при появлении этого экрана. Переход на финальный экран снимает фокус с предыдущего поля — именно это и отправляет его значение.

Сохраняйте каждое входное значение по мере их получения наблюдателем и отправляйте полный набор при закрытии флоу. Чтобы отловить этот момент, реализуйте `flowViewDidDisappear` на том же наблюдателе. Метод вызывается при закрытии экрана флоу — независимо от того, завершил ли пользователь флоу или закрыл его на полпути.

## Варианты использования \{#use-cases\}

### Регистрация пользователей на вашем бэкенде \{#register-users-on-your-backend\}

Собирайте значения по мере их поступления и отправляйте их одним запросом после закрытия флоу, чтобы передать полный набор ответов.

Экран закрывается как при завершении флоу, так и при его прерывании на полпути. Перед обращением к бэкенду проверяйте наличие нужных полей.

Флоу не может отображать ошибки бэкенда. Коллбэк не возвращает никакого значения, и в SDK нет метода для передачи данных в активный флоу. Если регистрация завершится с ошибкой — например, потому что email уже используется, — покажите её в собственном интерфейсе после закрытия флоу.

```dart showLineNumbers title="Flutter"
final Map<String, String> flowAnswers = {};

void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  if (name != 'flow_user_input') return;

  final elementId = params['element_id'];
  final value = params['value'];
  if (elementId is! String || value is! String) return;

  flowAnswers[elementId] = value;
}

void flowViewDidDisappear(AdaptyUIFlowView view) {
  if (!flowAnswers.containsKey('email')) return;

  // Send flowAnswers to your backend here to create the account.

  flowAnswers.clear();
}
```

### Обогащайте профили пользователей данными \{#enrich-user-profiles-with-data\}

Чтобы связать введённые пользователем данные с его профилем и не спрашивать одно и то же дважды, [обновляйте профиль пользователя](flutter-setting-user-attributes) по мере поступления значений.

Например, если в вашем флоу есть текстовое поле с Element ID `name` и поле для email с Element ID `email`:

```dart showLineNumbers title="Flutter"
void handleFlowInput(String name, Map<String, dynamic> params) async {
  if (name != 'flow_user_input') return;

  final value = params['value'];
  if (value is! String) return;

  final builder = AdaptyProfileParametersBuilder();

  switch (params['element_id'] as String) {
    case 'name':
      builder.setFirstName(value);
      break;
    case 'email':
      builder.setEmail(value);
      break;
    default:
      return;
  }

  try {
    await Adapty().updateProfile(builder.build());
  } on AdaptyError catch (adaptyError) {
    // handle the error
  }
}
```

### Настройте флоу, которые показываются позже \{#customize-flows-shown-later\}

Ответы на вопросы квиза также могут определять, что пользователь увидит на другом [плейсменте](placements) — другой флоу или другой пейвол внутри него.

Например, спросите пользователей об их опыте занятий спортом в онбординг-флоу, а затем покажите каждой группе свой флоу с разными продуктами и текстом.

1. Добавьте [квиз](onboarding-quizzes) в ваш флоу. Присвойте [выбираемой группе](flow-selectable-elements) Group ID `experience`, а каждому варианту — осмысленный Element ID.
2. Обработайте ответы и [задайте пользовательские атрибуты](flutter-setting-user-attributes) для пользователя.

```dart showLineNumbers title="Flutter"
void handleFlowInput(String name, Map<String, dynamic> params) async {
  if (name != 'flow_user_input') return;
  if (params['element_id'] != 'experience') return;

  final optionId = (params['item_ids'] as List).cast<String>().first;

  final builder = AdaptyProfileParametersBuilder();
  // Set the custom attribute 'experience' to the option the user selected
  // (beginner, amateur, or pro).
  builder.setCustomStringAttribute(optionId, 'experience');

  try {
    await Adapty().updateProfile(builder.build());
  } on AdaptyError catch (adaptyError) {
    // handle the error
  }
}
```

3. [Создайте сегмент](segments) для каждого значения пользовательского атрибута.
4. Создайте [плейсмент](placements) и добавьте [аудиторию](audience) для каждого сегмента.
5. [Отобразите флоу](flutter-present-paywalls) для этого плейсмента в вашем приложении.