在 Flutter SDK 中处理流程数据

当用户在流程中的输入框中输入内容、回答问题或拨动开关时,SDK 会通过其分析回调将数据传递给你的应用。

应用通常使用这些数据来:

  • 在自有后端注册用户:获取用户在引导流程中填写的邮箱和姓名,在流程结束时创建其账户。
  • 保存答案和偏好设置:记录用户的选择,供应用后续使用——例如,将其作为自定义属性写入用户的 Adapty 用户画像
  • 自定义后续流程:将问卷答案保存为自定义属性,然后针对后续版位进行定向,让每个市场细分看到不同的流程,或其中不同的付费墙。
  • 接入第三方分析平台:将答案转发至 Amplitude、Mixpanel 或你所使用的其他产品分析工具。

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

开始之前

您需要:

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

接收输入值

输入值会随其他所有来自流程的分析事件一起,通过事件名称 flow_user_input 到达同一个回调。在通过 AdaptyUI().setFlowsEventsObserver 注册的观察者上实现 flowViewDidReceiveAnalyticEvent

void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  handleFlowInput(name, params);
}

flowViewDidReceiveAnalyticEvent 回调会传递流程中的所有分析事件,包括页面浏览事件

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

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

应用接收输入的时机

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。使用其他语言查看流程的用户看到的文本可能不同。

事件示例

以下示例展示了每种事件的可用属性及注释中的示例值。

文本、邮箱、数字和电话输入(点击展开)
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)
}
日期、时间和日期时间选择器(点击展开)
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)
}
单选(点击展开)
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']
}
Multi choice (Click to expand)
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']
}
Toggle (Click to expand)
void flowViewDidReceiveAnalyticEvent(
  AdaptyUIFlowView view,
  String name,
  Map<String, dynamic> params,
) {
  params['element_id'];      // 'reminders'
  params['element_type'];    // 'toggle'
  params['value'];           // true   (bool)
}

传输与限制

Warning

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

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

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

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

使用场景

在您的后端注册用户

在数据陆续收到时先将其暂存,等流程关闭后再一次性发送,这样单次请求就能携带完整的答案集。

无论用户是完整完成了流程还是中途退出,视图都会消失。在调用后端接口之前,请先检查您所需的字段是否已齐备。

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

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

用户画像数据填充

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

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

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

自定义后续显示的流程

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

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

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