---
title: "Xử lý dữ liệu từ flow trong Android SDK"
description: "Lưu và sử dụng dữ liệu người dùng nhập vào trong flow của ứng dụng Android với 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`

Khi người dùng nhập vào một trường đầu vào, trả lời câu hỏi, hoặc bật/tắt một nút trong [flow](adapty-flow-builder), SDK sẽ truyền giá trị đó đến ứng dụng của bạn thông qua callback analytics.

Các ứng dụng thường sử dụng dữ liệu đó để:

- **Đăng ký người dùng trên backend của bạn**: Lấy địa chỉ email và tên người dùng đã nhập trong flow onboarding, rồi tạo tài khoản cho họ khi flow đóng lại.
- **Lưu câu trả lời và tùy chọn**: Theo dõi những gì người dùng đã chọn để ứng dụng có thể sử dụng sau này — ví dụ: [ghi vào hồ sơ người dùng Adapty của họ](android-setting-user-attributes) dưới dạng thuộc tính tùy chỉnh.
- **Tùy chỉnh các flow trong tương lai**: Lưu câu trả lời quiz dưới dạng thuộc tính tùy chỉnh, sau đó nhắm mục tiêu một placement sau để mỗi phân khúc nhận được một flow khác nhau, hoặc một paywall khác bên trong đó.
- **Gửi dữ liệu đến các nền tảng analytics bên thứ ba**: Chuyển tiếp câu trả lời đến Amplitude, Mixpanel, hoặc bất kỳ công cụ phân tích sản phẩm nào bạn đang sử dụng.

Các input và selectable group tự động báo cáo giá trị của chúng. Để phân biệt các input trong code, hãy đặt cho mỗi input một **Element ID** có ý nghĩa và mỗi selectable group một **Group ID** trong builder.

## Trước khi bắt đầu \{#before-you-start\}

Bạn cần:

- **Adapty SDK v4 trở lên**: Các flow callback không tồn tại trong các phiên bản cũ hơn.
- **Một flow được xây dựng trong [Flow & Paywall Builder](adapty-flow-builder)**: Chỉ có flow mới báo cáo các giá trị đầu vào thông qua callback này.
- **Một phiên bản flow đã được xuất bản gần đây**: Flow chỉ báo cáo các giá trị đầu vào nếu bạn xuất bản nó sau khi tính năng này có sẵn. Nếu không có gì đến được ứng dụng của bạn, hãy xuất bản một phiên bản mới của flow và thử lại.

## Nhận giá trị đầu vào \{#receive-input-values\}

Giá trị đầu vào được chuyển đến cùng callback với mọi sự kiện analytics khác từ một flow, dưới tên sự kiện `flow_user_input`. Ghi đè `onAnalyticEvent` trên listener sự kiện flow của bạn:

```kotlin showLineNumbers title="Kotlin"
class MyFlowEventListener : AdaptyFlowDefaultEventListener() {

    override fun onAnalyticEvent(
        name: String,
        params: Map<String, Any?>,
        context: Context,
    ) {
        handleFlowInput(name, params)
    }
}
```

Callback `onAnalyticEvent` truyền tất cả các sự kiện analytics từ một flow, bao gồm cả [lượt xem màn hình](android-flow-screen-views).
- Tham số `name` chứa tên sự kiện. Để lọc các sự kiện nhập liệu của người dùng, hãy so sánh `name` với `flow_user_input`.
- Tham số `element_type` cho biết danh mục của phần tử.
- Giá trị đầu vào được lưu ở các tham số khác nhau tùy theo loại phần tử:
     - Các trường văn bản, picker và toggle lưu dữ liệu nhập của người dùng trong `value`
     - Nhóm có thể chọn báo cáo các tùy chọn đang được chọn trong `item_ids` và `item_titles`

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, params: Map<String, Any?>) {
    if (name != "flow_user_input") return

    val elementId = params["element_id"] as? String ?: 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"] as? String

    when (params["element_type"]) {
        "text_input", "email_input", "number_input", "phone_input" -> {
            val text = params["value"] as? String
        }
        "date_picker", "time_picker", "date_time_picker" -> {
            // Unix time in milliseconds. Numbers arrive as Double — read them through Number.
            val millis = (params["value"] as? Number)?.toLong()
        }
        "single_choice" -> {
            val optionId = (params["item_ids"] as? List<*>)?.firstOrNull() as? String
        }
        "multi_choice" -> {
            val optionIds = (params["item_ids"] as? List<*>)?.filterIsInstance<String>()
        }
        "toggle" -> {
            val isOn = params["value"] as? Boolean
        }
    }
}
```

Để xác nhận callback được kích hoạt, hãy tương tác với input trong bản build thử nghiệm của ứng dụng. Nếu callback handler không nhận được sự kiện, hãy kiểm tra [điều kiện tiên quyết](#before-you-start). Đảm bảo rằng flow đã được xuất bản sau khi tính năng này có sẵn.

## Khi ứng dụng nhận đầu vào \{#when-your-app-receives-the-input\}

:::important
Giá trị nhập mặc định hoặc lựa chọn mặc định sẽ không bao giờ được gửi đến ứng dụng của bạn thông qua callback này. Nếu người dùng chấp nhận tùy chọn được đánh dấu **Set as default** và tiếp tục, sẽ không có sự kiện nào được kích hoạt. Đừng coi việc thiếu sự kiện là "chưa trả lời" — người dùng chỉ đơn giản là giữ nguyên giá trị mặc định.
:::

**Các thành phần sau đây kích hoạt sự kiện này:**

- Trường văn bản, email, số và điện thoại
- Bộ chọn ngày, giờ và ngày-giờ
- Nhóm lựa chọn đơn và đa lựa chọn, cùng với toggle

**Các thành phần sau đây không kích hoạt:**

- Trường mật khẩu, lựa chọn sản phẩm và chuyển tab
- Các trường nhập liệu bên trong phần tử Header, vốn được dùng chung trên nhiều màn hình
- Nhóm lựa chọn có Element ID trùng lặp hoặc bị thiếu, hoặc có Group ID được dùng lại trên màn hình khác. Nhóm như vậy sẽ không gửi gì cả, thay vì gửi một phần câu trả lời.

**Sự kiện được kích hoạt khi:**

- Một trường mất tiêu điểm (focus). Trường đã được xóa trắng sẽ báo cáo một chuỗi rỗng; trường người dùng chưa bao giờ chỉnh sửa sẽ không báo cáo gì. Placeholder không phải là giá trị. Nếu người dùng quay lại trường, chỉnh sửa rồi rời đi, một sự kiện thứ hai sẽ được gửi.
- Người dùng đóng bộ chọn sau khi chọn một giá trị mới. Đóng mà không thay đổi gì sẽ không báo cáo gì.
- Người dùng nhấn vào một tùy chọn hoặc toggle. Sự kiện đa lựa chọn liệt kê mọi tùy chọn đã được chọn, vì vậy việc bỏ chọn tùy chọn cuối cùng sẽ gửi hai mảng rỗng.

**Sự kiện không được kích hoạt khi:**

- Người dùng đang gõ. Không có luồng sự kiện gõ phím, chỉ có giá trị mà trường đang giữ khi tiêu điểm rời khỏi nó.
- Người dùng gửi hoặc đóng flow. Giá trị đang được chỉnh sửa tại thời điểm đó có thể bị mất; phần [giới hạn gửi](#delivery-and-limitations) đề cập đến cách thiết kế màn hình cuối cùng xung quanh vấn đề này.
- Một giá trị được đặt mà không có tương tác của người dùng. Tùy chọn được đánh dấu **Set as default** được chọn sẵn khi màn hình mở ra, và hành động **Set Variable** có thể chọn một tùy chọn hoặc điền vào trường nhập liệu từ một tương tác khác. Cả hai đều không gửi sự kiện; trường được điền sẵn chỉ được báo cáo một lần khi người dùng chỉnh sửa nó.

## Những gì bạn nhận được \{#what-you-receive\}

Callback truyền hai sự kiện. Lọc theo `name` để chỉ hiển thị các sự kiện `flow_user_input`. Payload JSON trả về có dạng như sau:

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

| Tham số           | Mô tả |
|:------------------|:------------|
| `name`            | `flow_user_input` cho sự kiện nhập liệu, `flow_screen_showed` cho lượt xem màn hình. |
| `instanceId`      | ID của màn hình chứa input. Element ID là duy nhất trong phạm vi một màn hình, không phải toàn bộ flow. Nếu flow của bạn có input trên nhiều màn hình, hãy kết hợp `instanceId` với `element_id` khi lọc sự kiện. |
| `element_id`      | **Element ID** của [input](builder-inputs-and-forms), hoặc **Group ID** của [nhóm có thể chọn](flow-selectable-elements). |
| `element_type`    | Loại element đã gửi sự kiện. Nó xác định tham số nào dưới đây chứa giá trị input. |
| `value`           | **Chỉ dành cho text field, picker và toggle.** Giá trị input: chuỗi ký tự cho text field, số nguyên cho picker, boolean cho toggle. |
| `item_ids`        | **Chỉ dành cho nhóm single-choice và multi-choice.** **Element ID** của các tùy chọn được chọn, theo thứ tự xuất hiện trong builder. Một mục cho nhóm single-choice; nhiều mục cho nhóm multi-choice. |
| `item_titles`     | **Chỉ dành cho nhóm single-choice và multi-choice.** Tiêu đề của các tùy chọn được liệt kê trong `item_ids`, theo cùng thứ tự. Không bao giờ rỗng: một tùy chọn không có tiêu đề sẽ báo cáo ID của nó. |
| `isCustomerEvent` | Cờ tiện ích, luôn là `true` cho sự kiện này. Đánh dấu các sự kiện mà SDK truyền đến callback của bạn. Hữu ích nếu một handler chuyển tiếp mọi sự kiện flow đến analytics của bạn và bạn kiểm tra theo cờ này thay vì theo `name`. |
| `isBackendEvent`  | Cờ tiện ích, luôn là `false` cho sự kiện này. Đánh dấu các sự kiện mà Adapty cũng ghi lại cho analytics của riêng mình. `false` xác nhận rằng nội dung người dùng nhập chỉ đến ứng dụng của bạn và không đi đâu khác — Adapty không nhận hay lưu trữ nó. |

Những gì mỗi element báo cáo:

| Trong builder | `element_type` | Tham số lưu giá trị | Nội dung |
|:---------------|:---------------|:----------------------------|:--------------|
| Input **Text**, **Number**, **Phone number** | `text_input`, `number_input`, `phone_input` | `value` | Chuỗi ký tự thô người dùng đã nhập. Số đến dưới dạng chuỗi, không phải kiểu số. |
| Input **E-mail** | `email_input` | `value` | Chuỗi ký tự thô người dùng đã nhập, kể cả khi không vượt qua xác thực định dạng của builder. Hãy tự xác thực ở phía bạn trước khi sử dụng. |
| Input **Password** | none | none | Không gửi sự kiện. |
| Input **Date** | `date_picker` | `value` | Unix time tính bằng mili giây, dưới dạng số nguyên, tại nửa đêm theo giờ địa phương của ngày được chọn. |
| Input **Time** | `time_picker` | `value` | Unix time tính bằng mili giây, dưới dạng số nguyên, làm tròn xuống theo phút. |
| Input **Date & Time** | `date_picker` và `time_picker` | `value` | Hai element: một date picker và một time picker. Mỗi element gửi sự kiện riêng. |
| Input được chuyển sang **Date & Time** trong dropdown **Type** | `date_time_picker` | `value` | Unix time tính bằng mili giây, dưới dạng số nguyên, làm tròn xuống theo phút. |
| Nhóm **Single choice** | `single_choice` | `item_ids`, `item_titles` | Hai mảng. `item_ids`: mảng chứa Element ID của tùy chọn được chọn. `item_titles`: mảng chứa tiêu đề của tùy chọn đó. |
| Nhóm **Multi-choice** | `multi_choice` | `item_ids`, `item_titles` | Hai mảng. `item_ids`: Element ID của mọi tùy chọn được chọn, theo thứ tự xuất hiện trong builder. `item_titles`: tiêu đề của chúng, theo cùng thứ tự. Cả hai mảng đều rỗng khi không có gì được chọn. |
| Nhóm **Toggle** | `toggle` | `value` | Một boolean. |

Để phân nhánh theo câu trả lời, hãy so sánh `item_ids`, không phải `item_titles`. Tiêu đề được suy ra: **Element Title** của tùy chọn nếu bạn đã đặt, nếu không thì văn bản trong ngôn ngữ mặc định của bạn, nếu không thì Element ID của nó. Người dùng xem flow bằng ngôn ngữ khác sẽ thấy văn bản khác.

## Ví dụ về sự kiện \{#event-examples\}

Những ví dụ này minh họa các thuộc tính có trên mỗi sự kiện, với các giá trị minh họa trong phần chú thích.

<Details>
<summary>Text, email, number, and phone input (Click to expand)</summary>

```kotlin
override fun onAnalyticEvent(name: String, params: Map<String, Any?>, context: Context) {
    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>Bộ chọn ngày, giờ và ngày-giờ (Nhấn để mở rộng)</summary>

```kotlin
override fun onAnalyticEvent(name: String, params: Map<String, Any?>, context: Context) {
    params["element_id"]       // "birthday"
    params["element_type"]     // "date_picker"
    params["value"]            // 6.45408E11   (Double — Unix milliseconds, 1990-06-15, local midnight)
}
```
</Details>

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

```kotlin
override fun onAnalyticEvent(name: String, params: Map<String, Any?>, context: Context) {
    params["element_id"]       // "experience"
    params["element_type"]     // "single_choice"
    params["item_ids"]         // ["pro"]   (List<String>)
    params["item_titles"]      // ["I train professionally"]
}
```
</Details>

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

```kotlin
override fun onAnalyticEvent(name: String, params: Map<String, Any?>, context: Context) {
    params["element_id"]       // "interests"
    params["element_type"]     // "multi_choice"
    params["item_ids"]         // ["sports", "music"]   (List<String>)
    params["item_titles"]      // ["Sports", "Music"]
}
```
</Details>

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

```kotlin
override fun onAnalyticEvent(name: String, params: Map<String, Any?>, context: Context) {
    params["element_id"]       // "reminders"
    params["element_type"]     // "toggle"
    params["value"]            // true   (Boolean)
}
```
</Details>

## Phân phối và giới hạn \{#delivery-and-limitations\}

:::warning
Flow gửi dữ liệu thô — địa chỉ email, số điện thoại, và bất cứ thứ gì người dùng nhập vào. Hãy coi mọi thứ mà analytics callback trả về đều là dữ liệu cá nhân, và đừng lưu trữ chúng ở bất kỳ nơi nào bạn không muốn lưu địa chỉ email của người dùng.
:::

- **Giá trị cuối cùng được ưu tiên**: Bạn nhận một sự kiện cho mỗi trường, mang theo giá trị mà người dùng đã chốt chứ không phải từng lần nhấn phím. Nếu họ chỉnh sửa một trường, chỉ phiên bản cuối cùng mới được gửi đến bạn.
- **Không đảm bảo submit**: Giá trị được gửi đến bạn khi người dùng di chuyển qua flow, và người dùng có thể thoát bất cứ lúc nào. Hãy đợi flow đóng lại trước khi coi một tập hợp câu trả lời là hoàn chỉnh.
- **Gửi theo khả năng tốt nhất**: Nếu người dùng đóng flow hoặc thu nhỏ ứng dụng trong khi một trường vẫn đang được focus hoặc một picker vẫn đang mở, giá trị đó có thể bị mất.

Để đảm bảo giá trị của trường cuối cùng luôn đáng tin cậy, hãy kết thúc flow bằng một màn hình không có ô nhập liệu và không có picker, đồng thời đóng flow bằng một thao tác rõ ràng của người dùng thay vì tự động đóng khi màn hình đó xuất hiện. Chuyển sang màn hình cuối sẽ lấy focus khỏi trường trước đó, đó chính là thao tác kích hoạt việc gửi giá trị.

Lưu từng giá trị input khi listener nhận được, và gửi toàn bộ khi flow đóng lại. Để bắt được thời điểm đó, hãy override `onFlowClosed` trong `AdaptyFlowEventListener` của bạn. Hàm này chạy khi flow view bị đóng, dù người dùng đã hoàn thành flow hay đóng giữa chừng.

## Các trường hợp sử dụng \{#use-cases\}

### Đăng ký người dùng trên backend của bạn \{#register-users-on-your-backend\}

Thu thập các giá trị khi chúng đến và gửi chúng một lần khi flow đóng lại, để một request mang đầy đủ tất cả câu trả lời.

Flow đóng lại dù người dùng đã hoàn thành hay bỏ dở giữa chừng. Hãy kiểm tra các trường bạn cần trước khi gọi backend.

Flow không thể hiển thị lỗi từ backend của bạn. Callback không có giá trị trả về, và SDK không có phương thức nào gửi dữ liệu vào flow đang chạy. Nếu đăng ký thất bại, chẳng hạn do email đã được sử dụng, hãy hiển thị lỗi trong giao diện của bạn sau khi flow đóng lại.

```kotlin showLineNumbers title="Kotlin"
class MyFlowEventListener : AdaptyFlowDefaultEventListener() {

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

    override fun onAnalyticEvent(
        name: String,
        params: Map<String, Any?>,
        context: Context,
    ) {
        if (name != "flow_user_input") return

        val elementId = params["element_id"] as? String ?: return
        val value = params["value"] as? String ?: return

        flowAnswers[elementId] = value
    }

    override fun onFlowClosed() {
        if (!flowAnswers.containsKey("email")) return

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

        flowAnswers.clear()
    }
}
```

### Làm phong phú hồ sơ người dùng với dữ liệu \{#enrich-user-profiles-with-data\}

Để liên kết dữ liệu người dùng nhập với hồ sơ của họ và tránh hỏi lại thông tin đã có, hãy [cập nhật hồ sơ người dùng](android-setting-user-attributes) ngay khi nhận được giá trị.

Ví dụ, nếu flow của bạn có ô nhập văn bản với Element ID là `name` và ô nhập email với Element ID là `email`:

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, params: Map<String, Any?>) {
    if (name != "flow_user_input") return

    val elementId = params["element_id"] as? String ?: return
    val value = params["value"] as? String ?: return

    val builder = AdaptyProfileParameters.Builder()

    when (elementId) {
        "name" -> builder.withFirstName(value)
        "email" -> builder.withEmail(value)
        else -> return
    }

    Adapty.updateProfile(builder.build()) { error ->
        if (error != null) {
            // handle the error
        }
    }
}
```

### Tùy chỉnh các flow hiển thị sau \{#customize-flows-shown-later\}

Câu trả lời quiz cũng có thể quyết định những gì người dùng thấy tại một [placement](placements) sau đó — một flow khác, hoặc một paywall khác bên trong đó.

Ví dụ: hỏi người dùng về kinh nghiệm thể thao của họ trong onboarding flow, sau đó hiển thị cho mỗi nhóm flow riêng với các sản phẩm và nội dung khác nhau.

1. Thêm một [quiz](onboarding-quizzes) vào flow của bạn. Đặt cho [selectable group](flow-selectable-elements) Group ID là `experience`, và mỗi tùy chọn một Element ID có ý nghĩa.
2. Xử lý các câu trả lời và [thiết lập custom attributes](android-setting-user-attributes) cho người dùng.

```kotlin showLineNumbers title="Kotlin"
private fun handleFlowInput(name: String, params: Map<String, Any?>) {
    if (name != "flow_user_input") return
    if (params["element_id"] != "experience") return

    val optionId = (params["item_ids"] as? List<*>)?.firstOrNull() as? String ?: return

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

    Adapty.updateProfile(builder.build()) { error ->
        if (error != null) {
            // handle the error
        }
    }
}
```

3. [Tạo một phân khúc](segments) cho mỗi giá trị thuộc tính tùy chỉnh.
4. Tạo một [placement](placements) và thêm một [đối tượng](audience) cho mỗi phân khúc.
5. [Hiển thị flow](android-present-paywalls) cho placement đó trong ứng dụng của bạn.