在 Kotlin Multiplatform SDK 中处理流程数据

当用户在流程中的输入框输入内容、回答问卷或切换开关时,SDK 会通过分析回调将该值传递给你的应用。

应用最常将这些数据用于:

  • 在自有后端注册用户:获取用户在引导流程中填写的邮箱地址和姓名,在流程关闭时为其创建账号。
  • 保存答案和偏好设置:记录用户的选择,以便应用后续使用——例如,将其写入用户的 Adapty 用户画像作为自定义属性。
  • 个性化未来流程:将问卷答案保存为自定义属性,然后针对后续版位进行定向,让每个市场细分看到不同的流程,或流程中不同的付费墙。
  • 向第三方分析平台传送数据:将答案转发给 Amplitude、Mixpanel 或您使用的任何产品分析工具。

输入框和可选组会自动上报其值。为了在代码中区分各个输入框,请在编辑工具中为每个输入框指定一个有意义的 Element ID,为每个可选组指定一个 Group ID

开始之前

您需要:

  • Adapty SDK v4 或更高版本:流程回调在旧版本中不存在。
  • Flow & Paywall Builder 中构建的流程:只有流程才会通过此回调上报输入值。
  • 最近发布的流程版本:流程仅在您发布此功能上线后的新版本时才会上报输入值。如果您的应用没有收到任何内容,请重新发布一个新版本的流程后再试。

接收输入值

输入值通过与其他所有流程分析事件相同的回调传递,事件名称为 flow_user_input。在通过 AdaptyUI.setFlowsEventsObserver 注册的观察者上重写 flowViewDidReceiveAnalyticEvent

AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver {

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

flowViewDidReceiveAnalyticEvent 回调会传递流程中的所有分析事件,包括屏幕浏览事件。事件参数以单个 JSON 字符串的形式通过 paramsJsonString 传入,解码一次后即可读取各字段。

  • name 参数包含事件名称。若要筛选用户输入事件,请将 nameflow_user_input 进行比较。
  • element_type 参数标识元素类别。
  • 输入值根据元素类型存储在不同的参数中:
    • 文本字段、选择器和开关将用户输入存储在 value
    • 可选组在 item_idsitem_titles 中报告当前选中的选项
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
        }
    }
}

要确认回调是否触发,请在你自己的应用测试版本中与输入进行交互。如果你的回调处理程序未收到事件,请检查前提条件。请确保该流程是在此功能可用之后才发布的。

应用接收输入的时机

Important

默认输入值或选中值不会通过此回调传递到你的应用。如果用户接受了标记为 Set as default 的选项并继续操作,不会触发任何事件。不要把缺少事件理解为”未作答”——用户只是保留了默认值。

以下元素会触发此事件:

  • 文本、电子邮件、数字和电话输入框
  • 日期、时间和日期时间选择器
  • 单选和多选组以及开关

以下元素不会触发:

  • 密码输入框、产品选择器和标签切换
  • Header 元素内部的输入框(该元素在所有屏幕之间共享)
  • 存在重复或缺失选项 Element ID 的可选组,或者 Group ID 在另一个屏幕上被复用的可选组。此类选项组不会发送任何内容,而非发送部分答案。

以下情况会触发此事件:

  • 输入框失去焦点时。已清空的输入框报告空字符串;用户从未编辑过的输入框不报告任何内容。占位符不是值。如果用户返回输入框、编辑后再次离开,则会触发第二次事件。
  • 用户在选择新值后关闭选择器时。未做修改直接关闭不会报告任何内容。
  • 用户点击某个选项或开关时。多选事件会列出所有已选选项,因此取消选中最后一个选项时会发送两个空数组。

以下情况不会触发此事件:

  • 用户正在输入时。不存在按键流,只有输入框失去焦点时所持有的值。
  • 用户提交或关闭流程时。此时仍处于编辑状态的值可能会丢失;投递限制一节介绍了如何针对最后一屏进行相应的设计。
  • 在无用户交互的情况下设置值时。标记为 Set as default 的选项在屏幕打开时即为预选状态,Set Variable 操作可以根据其他交互来选中选项或填充输入框。两者均不会发送事件;预填充的输入框只有在用户编辑后才会报告。

您将收到的内容

回调会传递两个事件。通过 name 进行过滤,只处理 flow_user_input 事件。响应的 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 在同一屏幕内唯一,在整个流程中不保证唯一。如果流程中有多个屏幕包含输入框,过滤事件时需将 instanceIdelement_id 结合使用。
element_id输入框Element ID,或可选择分组Group ID
element_type发送事件的元素类型,决定以下哪个参数携带输入值。
value仅适用于文本字段、选择器和开关。 输入值:文本字段为字符串,选择器为整数,开关为布尔值。
item_ids仅适用于单选和多选分组。 已选项的 Element ID,顺序与编辑工具中选项的排列顺序一致。单选组有一个条目,多选组可有任意数量。
item_titles仅适用于单选和多选分组。 item_ids 中各选项对应的标题,顺序相同。该字段不会为空:没有标题的选项会上报其 ID。
isCustomerEvent工具标志位,此事件始终为 true。用于标记 SDK 传递给回调的事件。如果一个处理器将所有流程事件转发至埋点系统,可通过此标志而非 name 进行过滤。
isBackendEvent工具标志位,此事件始终为 false。用于标记 Adapty 同步记录至自身分析系统的事件。false 表示用户输入的内容仅到达你的应用,不会发送到其他地方——Adapty 不会接收或存储这些数据。

各元素上报内容说明:

编辑工具中的元素element_type存储值的参数内容说明
文本数字电话号码输入框text_inputnumber_inputphone_inputvalue用户输入的原始字符串。数字以字符串形式传递,不是数值类型。
邮箱输入框email_inputvalue用户输入的原始字符串,即使未通过编辑工具的格式校验也会上报。使用前请在你的一侧进行验证。
密码输入框不发送任何事件。
日期输入框date_pickervalue所选日期本地零点的 Unix 时间戳(毫秒),整数类型。
时间输入框time_pickervalue精确到分钟的 Unix 时间戳(毫秒),整数类型。
日期 & 时间输入框date_pickertime_pickervalue包含两个元素:日期选择器和时间选择器,各自发送独立事件。
Type 下拉菜单中切换为 Date & Time 的输入框date_time_pickervalue精确到分钟的 Unix 时间戳(毫秒),整数类型。
单选分组single_choiceitem_idsitem_titles两个数组。item_ids:包含已选项 Element ID 的数组。item_titles:包含该选项标题的数组。
多选分组multi_choiceitem_idsitem_titles两个数组。item_ids:所有已选项的 Element ID,顺序与编辑工具中的排列一致。item_titles:对应的标题,顺序相同。未选中任何选项时两个数组均为空。
开关分组togglevalue布尔值。

根据答案进行分支判断时,请比较 item_ids,而非 item_titles。标题是派生值:优先使用选项的 Element Title(如已设置),其次是默认语言环境中的文本,最后是 Element ID。使用其他语言查看流程的用户看到的文本可能不同。

事件示例

这些示例展示了每个事件可用的属性,注释中包含示意性的值。

文本、邮箱、数字和电话号码输入(点击展开)
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"
}
日期、时间和日期时间选择器(点击展开)
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)
}
单选(点击展开)
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"]
}
Multi choice (Click to expand)
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"]
}
Toggle (Click to expand)
override fun flowViewDidReceiveAnalyticEvent(
    view: AdaptyUIFlowView,
    name: String,
    paramsJsonString: String,
) {
    // paramsJsonString, once decoded:
    params["element_id"]       // "reminders"
    params["element_type"]     // "toggle"
    params["value"]            // true
}

传输与限制

Warning

流程会发送原始值——电子邮件地址、电话号码以及用户输入的任何其他内容。请将分析回调传递的所有内容视为个人数据,不要将其写入任何你不会写入用户电子邮件地址的地方。

  • 后值覆盖前值:每个字段只触发一个事件,携带的是用户最终确认的值,而非逐键输入的流。如果用户编辑了某个字段,只有最后一个版本会传递给你。
  • 无提交保证:值会在用户浏览流程时传递给你,用户可以在任意时刻离开。在将一组答案视为完整之前,请等待流程关闭。
  • 尽力传递:如果用户在某个字段仍处于焦点状态或选择器仍处于打开状态时关闭流程或将应用切换到后台,该值可能会丢失。

为确保最后一个字段的值可靠传递,请以一个没有输入框和选择器的屏幕结束流程,并通过用户的明确操作(而非该屏幕出现时自动)关闭流程。跳转到最终屏幕会使前一个字段失去焦点,从而触发其值的发送。

将每个输入值存储到观察者接收时,并在流程关闭时发送完整的数据集。要捕捉该时机,请在同一观察者上重写 flowViewDidDisappear。无论用户是完成流程还是中途关闭,该方法都会在流程视图消失时执行。

使用场景

在后端注册用户

收集答案时请逐步积累,等流程关闭后再一次性发送请求,确保单次请求包含完整的答案集。

无论用户完成流程还是中途退出,界面都会消失。在调用后端接口之前,请先校验所需字段是否已填写。

流程无法显示来自后端的错误信息。回调没有返回值,SDK 也没有向运行中的流程传入数据的方法。如果注册失败(例如邮箱已被占用),请在流程关闭后通过自己的 UI 展示错误提示。

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()
    }
}

使用数据丰富用户画像

为了将用户输入的内容与其用户画像关联,并避免重复询问相同信息,请在收到数据时更新用户画像

例如,如果你的流程中有一个 Element ID 为 name 的文本输入框和一个 Element ID 为 email 的邮箱输入框:

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
            }
    }
}

自定义后续显示的流程

测验答案也可以决定用户在后续版位中看到的内容——不同的流程,或其中不同的付费墙。

例如,在用户引导流程中询问用户的运动经验,然后为每组用户展示包含不同产品和文案的专属流程。

  1. 在流程中添加一个测验。将可选组的 Group ID 设置为 experience,并为每个选项指定有意义的 Element ID。
  2. 处理答案并为用户设置自定义属性
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
            }
    }
}
  1. 为每个自定义属性值创建一个市场细分
  2. 创建一个版位,并为每个市场细分添加一个目标受众
  3. 在你的应用中展示该版位的流程