---
title: "Обработка данных из флоу в Kotlin Multiplatform SDK"
description: "Сохраняйте и используйте данные, которые пользователи вводят во флоу, в своём Kotlin Multiplatform приложении с помощью 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](kmp-setting-user-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`:

```kotlin showLineNumbers title="Kotlin"
AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver {

    override fun flowViewDidReceiveAnalyticEvent(
        view: AdaptyUIFlowView,
        name: String,
        paramsJsonString: String,
    ) {
        handleFlowInput(name, paramsJsonString)
    }
})
```

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

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, paramsJsonString: String) {
    if (name != "flow_user_input") return

    val params = Json.parseToJsonElement(paramsJsonString).jsonObject
    val elementId = params["element_id"]?.jsonPrimitive?.content ?: return

    // The screen the input sits on. Pair it with elementId to tell apart
    // two fields that share an Element ID on different screens.
    val screenId = params["instanceId"]?.jsonPrimitive?.contentOrNull

    when (params["element_type"]?.jsonPrimitive?.content) {
        "text_input", "email_input", "number_input", "phone_input" -> {
            val text = params["value"]?.jsonPrimitive?.content
        }
        "date_picker", "time_picker", "date_time_picker" -> {
            // Unix time in milliseconds. Read it as a double — Android serializes numbers that way.
            val millis = params["value"]?.jsonPrimitive?.double?.toLong()
        }
        "single_choice" -> {
            val optionId = params["item_ids"]?.jsonArray?.firstOrNull()?.jsonPrimitive?.content
        }
        "multi_choice" -> {
            val optionIds = params["item_ids"]?.jsonArray?.map { it.jsonPrimitive.content }
        }
        "toggle" -> {
            val isOn = params["value"]?.jsonPrimitive?.boolean
        }
    }
}
```

Чтобы убедиться, что коллбэк срабатывает, взаимодействуйте с полем ввода в тестовой сборке вашего приложения. Если обработчик коллбэка не получает событие, проверьте [предварительные требования](#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>

```kotlin
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    name                       // "flow_user_input"
    paramsJsonString           // "{\"name\":\"flow_user_input\",\"instanceId\":\"scr_J260KU5q\",\"isBackendEvent\":false,\"isCustomerEvent\":true,\"element_id\":\"email\",\"element_type\":\"email_input\",\"value\":\"jane@example.com\"}"

    // paramsJsonString, once decoded:
    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"
}
```
</Details>

<Details>
<summary>Выбор даты, времени и даты+времени (Click to expand)</summary>

```kotlin
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    // paramsJsonString, once decoded:
    params["element_id"]       // "birthday"
    params["element_type"]     // "date_picker"
    params["value"]            // 645408000000   (Unix milliseconds — 1990-06-15, local midnight)
}
```
</Details>

<Details>
<summary>Одиночный выбор (Click to expand)</summary>

```kotlin
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    // paramsJsonString, once decoded:
    params["element_id"]       // "experience"
    params["element_type"]     // "single_choice"
    params["item_ids"]         // ["pro"]
    params["item_titles"]      // ["I train professionally"]
}
```
</Details>

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

```kotlin
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    // paramsJsonString, once decoded:
    params["element_id"]       // "interests"
    params["element_type"]     // "multi_choice"
    params["item_ids"]         // ["sports", "music"]
    params["item_titles"]      // ["Sports", "Music"]
}
```
</Details>

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

```kotlin
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    // paramsJsonString, once decoded:
    params["element_id"]       // "reminders"
    params["element_type"]     // "toggle"
    params["value"]            // true
}
```
</Details>

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

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

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

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

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

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

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

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

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

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

```kotlin showLineNumbers title="Kotlin"
class MyFlowsEventsObserver : AdaptyUIFlowsEventsObserver {

    private val flowAnswers = mutableMapOf<String, String>()

    override fun flowViewDidReceiveAnalyticEvent(
        view: AdaptyUIFlowView,
        name: String,
        paramsJsonString: String,
    ) {
        if (name != "flow_user_input") return

        val params = Json.parseToJsonElement(paramsJsonString).jsonObject
        val elementId = params["element_id"]?.jsonPrimitive?.content ?: return
        val value = params["value"]?.jsonPrimitive?.contentOrNull ?: return

        flowAnswers[elementId] = value
    }

    override fun flowViewDidDisappear(view: AdaptyUIFlowView) {
        if (!flowAnswers.containsKey("email")) return

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

        flowAnswers.clear()
    }
}
```

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

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

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

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, paramsJsonString: String) {
    if (name != "flow_user_input") return

    val params = Json.parseToJsonElement(paramsJsonString).jsonObject
    val value = params["value"]?.jsonPrimitive?.contentOrNull ?: return

    val builder = AdaptyProfileParameters.Builder()

    when (params["element_id"]?.jsonPrimitive?.content) {
        "name" -> builder.withFirstName(value)
        "email" -> builder.withEmail(value)
        else -> return
    }

    mainUiScope.launch {
        Adapty.updateProfile(builder.build())
            .onError { error ->
                // handle the error
            }
    }
}
```

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

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

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

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

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, paramsJsonString: String) {
    if (name != "flow_user_input") return

    val params = Json.parseToJsonElement(paramsJsonString).jsonObject
    if (params["element_id"]?.jsonPrimitive?.content != "experience") return

    val optionId = params["item_ids"]?.jsonArray?.firstOrNull()
        ?.jsonPrimitive?.contentOrNull ?: return

    val builder = AdaptyProfileParameters.Builder()
    // Set the custom attribute 'experience' to the option the user selected
    // (beginner, amateur, or pro).
    builder.withCustomAttribute("experience", optionId)

    mainUiScope.launch {
        Adapty.updateProfile(builder.build())
            .onError { error ->
                // handle the error
            }
    }
}
```

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