Kotlin Multiplatform SDKでフローのデータを処理する

ユーザーがフローの入力フィールドに入力したり、クイズに回答したり、トグルを切り替えたりすると、SDKはその値をアナリティクスコールバックを通じてアプリに渡します。

アプリでこのデータを活用する主な用途は次のとおりです:

  • ユーザーを自社バックエンドに登録する: オンボーディングフローでユーザーが入力したメールアドレスと名前を取得し、フローが閉じられたタイミングでアカウントを作成します。
  • 回答と設定を保存する: ユーザーが選択した内容を追跡して、後でアプリから活用できるようにします。たとえば、カスタム属性としてAdapty プロファイルに書き込むといった使い方ができます。
  • 今後のフローをカスタマイズする: クイズの回答をカスタム属性として保存し、後続のプレースメントでターゲティングすることで、各セグメントに異なるフローや、その中の異なるペイウォールを表示できます。
  • サードパーティの分析プラットフォームに連携する: Amplitude や Mixpanel など、利用中のプロダクト分析ツールに回答を転送します。

インプットとセレクタブルグループは、値を自動的にレポートします。コード内でインプットを識別するために、各インプットに意味のある Element ID を、各セレクタブルグループに Group ID をビルダーで設定してください。

始める前に

以下が必要です:

  • Adapty SDK v4以降: フローのコールバックは以前のバージョンには存在しません。
  • フロー&ペイウォールビルダーで作成されたフロー: コールバックを通じて入力値を報告するのはフローのみです。
  • 最近公開されたフローのバージョン: フローがこの機能が利用可能になった後に公開された場合にのみ、入力値を報告します。アプリに何も届かない場合は、フローの新しいバージョンを公開して再試行してください。

入力値の受け取り

入力値は、フローからの他のすべてのアナリティクスイベントと同じコールバックに、イベント名 flow_user_input として届きます。AdaptyUI.setFlowsEventsObserver で登録したオブザーバーの flowViewDidReceiveAnalyticEvent をオーバーライドしてください:

AdaptyUI.setFlowsEventsObserver(object : AdaptyUIFlowsEventsObserver {

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

flowViewDidReceiveAnalyticEventコールバックは、画面ビューを含むフローのすべての分析イベントを配信します。イベントパラメータはparamsJsonStringに1つのJSON文字列として届きます。一度デコードしてフィールドを読み取ってください。

  • 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 が再利用されているグループ。このようなグループは、部分的な回答ではなく、何も送信しません。

イベントがトリガーされるタイミング:

  • フィールドがフォーカスを失ったとき。クリアされたフィールドは空文字列を報告し、ユーザーが一度も編集しなかったフィールドは何も報告しません。プレースホルダーは値ではありません。ユーザーがフィールドに戻って編集し、再びフォーカスを外した場合、2回目のイベントが発生します。
  • ユーザーが新しい値を選択した後にピッカーを閉じたとき。値を変えずに閉じた場合は何も報告されません。
  • ユーザーがオプションまたはトグルをタップしたとき。複数選択イベントはすべての選択済みオプションを列挙するため、最後の1つを選択解除すると2つの空の配列が送信されます。

イベントがトリガーされないタイミング:

  • ユーザーが入力中のとき。キーストリームは存在せず、フォーカスが外れた時点でフィールドが保持している値のみが送信されます。
  • ユーザーがフローを送信または閉じたとき。その時点で編集中の値は失われる可能性があります。配信の制限事項 セクションでは、最後の画面をこれに合わせて設計する方法を説明しています。
  • ユーザーの操作なしに値が設定されたとき。Set as default とマークされたオプションは画面が開いたときに事前選択されており、Set Variable アクションにより別の操作からオプションを選択したり入力フィールドを埋めたりできます。どちらもイベントを送信しません。事前入力済みの入力は、ユーザーが編集した時点で初めて報告されます。

受け取れる情報

コールバックは2つのイベントを配信します。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(ビルダーでの表示順)。単一選択グループでは1件、複数選択グループでは任意の件数。
item_titles単一選択・複数選択の選択可能グループのみ。 item_ids に記載されたオプションのタイトル(同じ順序)。空になることはありません。タイトルのないオプションはIDが代わりに報告されます。
isCustomerEventユーティリティフラグ。このイベントでは常に true。SDKがコールバックに配信するイベントを示します。1つのハンドラーがすべてのフローイベントをアナリティクスに転送し、name ではなくこのフラグで分岐する場合に便利です。
isBackendEventユーティリティフラグ。このイベントでは常に false。AdaptyがAdapty自身のアナリティクス用に記録するイベントを示します。false は、ユーザーが入力した内容がアプリにのみ届き、他には渡らないことを確認します。Adaptyはその内容を受信・保存しません。

各要素が報告する内容:

ビルダー上の要素element_type値を格納するパラメーター保持する内容
TextNumberPhone number 入力text_inputnumber_inputphone_inputvalueユーザーが入力した生の文字列。数値も文字列として届きます(数値型ではありません)。
E-mail 入力email_inputvalueユーザーが入力した生の文字列。ビルダーの書式バリデーションに失敗した場合も含みます。使用前にアプリ側でバリデーションしてください。
Password 入力なしなしイベントを送信しません。
Date 入力date_pickervalue選択された日付のローカル時刻の深夜0時をUnix時間(ミリ秒、整数)で表した値。
Time 入力time_pickervalueUnix時間(ミリ秒、整数)で、分未満を切り捨てた値。
Date & Time 入力date_pickertime_pickervalue日付ピッカーと時刻ピッカーの2要素。それぞれが個別のイベントを送信します。
Type ドロップダウンで Date & Time に切り替えた入力date_time_pickervalueUnix時間(ミリ秒、整数)で、分未満を切り捨てた値。
Single choice グループsingle_choiceitem_idsitem_titles2つの配列。item_ids:選択されたオプションのElement IDを含む配列。item_titles:そのオプションのタイトルを含む配列。
Multi-choice グループmulti_choiceitem_idsitem_titles2つの配列。item_ids:選択されたすべてのオプションのElement ID(ビルダーでの表示順)。item_titles:同じ順序でのタイトル。何も選択されていない場合、両方の配列は空になります。
Toggle グループtogglevalueブール値。

回答で分岐する場合は、item_titles ではなく item_ids で比較してください。タイトルは派生値です。設定されていればオプションの 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"]
}
複数選択(クリックして展開)
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

フローは生の値(メールアドレス、電話番号、ユーザーが入力したその他の情報)を送信します。アナリティクスコールバックが届けるものはすべて個人データとして扱い、メールアドレスを書き残さないような場所には保存しないでください。

  • 最後の値が優先されます: フィールドごとに1つのイベントが届き、キーストロークのストリームではなくユーザーが最終的に入力した値が含まれます。フィールドを編集した場合、最後のバージョンのみが届きます。
  • 送信の保証はありません: ユーザーがフローを進むにつれて値が届きますが、ユーザーはどの時点でも離脱できます。一連の回答が完成したと判断する前に、フローが閉じるのを待ってください。
  • ベストエフォート方式での配信: フィールドがフォーカスされているときやピッカーが開いているときにユーザーがフローを閉じたりアプリをバックグラウンドに移動したりすると、その値が失われる場合があります。

最後のフィールドの値を確実に受け取るには、入力フィールドもピッカーもない画面でフローを終了し、その画面が表示されたときに自動的に閉じるのではなく、明示的なユーザー操作によってフローを閉じてください。最後の画面に移動すると前のフィールドからフォーカスが外れ、それが値の送信をトリガーします。

各入力値はオブザーバーが受け取った時点で保存し、フローが閉じられたときに完全なセットを送信します。そのタイミングを捉えるには、同じオブザーバーで 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. フローにクイズを追加します。選択可能グループのグループIDを experience に設定し、各オプションにわかりやすい要素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. アプリ内でそのプレースメントのフローを表示します。