カスタム A2UI コンポーネントを実装する

A2UI アーキテクチャでは、すべてのサーフェスがコンポーネント カタログによって駆動されます。AI エージェントが独自の UI プリミティブを考案したり、任意のコードを生成したりするのではなく、カタログでエージェントが利用できるコンポーネント、プロパティ スキーマ、機能を宣言します。エージェントは、これらのコンポーネントを使用してユーザー インターフェースを構築します。

アプリの設計システム用にカスタム カタログをビルドする際は、カタログの定義を具体的な Jetpack Compose UI 要素にマッピングするコンポーネントを実装します。各 A2UI コンポーネント(A2uiComponent)は、プロパティ スキーマ コントラクトを定義し、動的データが到着すると準備状況を評価し、データモデルからリアクティブ プロパティをバインドし、Compose UI を出力し、ユーザー操作アクションをエージェントにディスパッチします。

Compose UI レンダラ(androidx.a2ui.compose:compose-ui)は、アプリのデザイン システムに沿ったカスタム コンポーネントの実装に必要なインターフェースとレシーバー スコープを提供します。

静的型指定されたコンポーネント プロパティを宣言する

レンダリングの前に、コンポーネントがエージェントから受け取るプロパティを宣言します。ランタイム レイヤは、JSON スキーマの生成と実行時の値の抽出の両方に使用される静的型指定の A2uiProperty API を提供します。

// Define static properties, dynamic bindings, and component references
val textProp = A2uiProperty.dynamicString("text", required = true)
val variantProp = A2uiProperty.stringEnum("variant", enumValues = listOf("body", "title"))
val childProp = A2uiProperty.componentId("child", required = true)
val actionProp = A2uiProperty.action("action", required = true)

A2uiComponent インターフェースを実装する

A2uiComponent インターフェースを実装して、コンポーネントのスキーマを定義し、エージェントから受け取ったプロパティを Compose UI にマッピングします。

object CustomTextComponent : A2uiComponent {
    private val textProp = A2uiProperty.dynamicString("text", required = true)
    private val variantProp = A2uiProperty.stringEnum(
        "variant",
        enumValues = listOf("body", "title"),
    )

    override val name = "Text"
    override val description = "Displays dynamic text."
    override val properties = listOf(textProp, variantProp)

    @Composable
    override fun A2uiComponentScope.isReady(properties: A2uiComponentProperties): Boolean {
        // The component does not become ready until dynamic text data arrives
        return properties.bind(textProp) != null
    }

    @Composable
    override fun A2uiComponentScope.Content(
        properties: A2uiComponentProperties,
        modifier: Modifier,
    ) {
        // Reactively resolve dynamic data binding and subscribe to updates
        val text = properties.bind(textProp) ?: ""

        // Read the static configuration property
        val variant = properties[variantProp] ?: "body"
        val textStyle = if (variant == "title") {
            MaterialTheme.typography.titleLarge
        } else {
            MaterialTheme.typography.bodyLarge
        }

        Text(
            text = text,
            style = textStyle,
            modifier = modifier,
        )
    }
}

通常のデータモデル バインディングと双方向データモデル バインディングを解決する

コンポーネントの実装では、A2uiComponentScope を使用して動的にバインドされたプロパティを解決します。通常の動的プロパティの場合、bind は現在の値を返し、データモデルの更新を自動的にサブスクライブします。

インタラクティブ入力コンポーネントの場合、bindUpdater は安定したアップデータ ラムダを返します。書き込み可能なデータパスではなくリテラル文字列がエージェントから提供された場合、アップデータ ラムダは null になり、フィールドが読み取り専用であることを示します。

val labelProp = A2uiProperty.dynamicString("label", required = true)
val valueProp = A2uiProperty.dynamicBoolean("value")

@Composable
fun A2uiComponentScope.CustomCheckbox(properties: A2uiComponentProperties) {
    // Read a dynamic property from the data model subscribing to updates
    val label = properties.bind(labelProp) ?: ""

    // Bind a property value and its updater to handle two-way data binding
    val checked = properties.bind(valueProp) ?: false
    val onCheckedChange = properties.bindUpdater(valueProp)

    Row(verticalAlignment = Alignment.CenterVertically) {
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange,
            enabled = (onCheckedChange != null), // Read-only if no writable path was bound
        )
        Text(text = label)
    }
}

ユーザー操作をエージェントにディスパッチする

インタラクティブ コンポーネントは A2uiComponentScope.dispatchAction を使用して、ユーザー イベントをエージェントに返送します。

object CustomButtonComponent : A2uiComponent {
    private val childProp = A2uiProperty.componentId("child", required = true)
    private val actionProp = A2uiProperty.action("action", required = true)

    override val name = "Button"
    override val description = "A clickable button."
    override val properties = listOf(childProp, actionProp)

    @Composable
    override fun A2uiComponentScope.Content(
        properties: A2uiComponentProperties,
        modifier: Modifier,
    ) {
        val actionDefinition = properties[actionProp]
        val childId = properties[childProp] ?: return
        val currentAction by rememberUpdatedState(actionDefinition)
        val onClick: () -> Unit = remember {
            { currentAction?.let { dispatchAction(it) } }
        }

        Button(onClick = onClick, modifier = modifier) {
            val childState = observeA2uiComponentState(id = childId)
            when (childState) {
                is A2uiComponentState.Loading -> CircularProgressIndicator()
                is A2uiComponentState.Error -> Text("Error")
                is A2uiComponentState.Success -> A2uiComponent(childState.component)
            }
        }
    }
}

子コンポーネントとプログレッシブ レンダリングを処理する

ネストされた子をサポートするコンポーネントは、observeA2uiComponentState(id) を使用して子の状態を監視します。これにより、親コンテナがシェルをレンダリングし、子コンポーネントが個別に読み込まれるプログレッシブ レンダリングが可能になります。

val headerChildProp = A2uiProperty.componentId("headerId", required = true)

@Composable
fun A2uiComponentScope.CustomCompositeContent(
    properties: A2uiComponentProperties,
) {
    val headerId = properties[headerChildProp] ?: return

    val headerState = observeA2uiComponentState(id = headerId)
    when (headerState) {
        is A2uiComponentState.Loading -> {
            // Render a localized loading placeholder
            LinearProgressIndicator()
        }
        is A2uiComponentState.Error -> {
            // Render a localized error fallback
            Text("Failed to load header")
        }
        is A2uiComponentState.Success -> {
            // Forward the resolved child component to the visual UI router
            A2uiComponent(headerState.component)
        }
    }
}

子(列、行、リストのアイテムなど)のコレクションまたはリストを処理するには、A2uiProperty.childList を使用してプロパティを宣言し、bindChildReferences を使用して子を解決します。

val childrenProp = A2uiProperty.childList("children", required = true)

@Composable
fun A2uiComponentScope.CustomColumn(
    properties: A2uiComponentProperties,
    modifier: Modifier = Modifier,
) {
    // Resolve child references (supports both static ID arrays and dynamic data templates)
    val childReferences = properties.bindChildReferences(childrenProp) ?: return

    Column(modifier = modifier) {
        childReferences.forEach { reference ->
            key(reference.id, reference.baseDataPath) {
                val childState = observeA2uiComponentState(reference)
                when (childState) {
                    is A2uiComponentState.Loading -> CircularProgressIndicator()
                    is A2uiComponentState.Error -> Text("Failed to load child")
                    is A2uiComponentState.Success -> A2uiComponent(childState.component)
                }
            }
        }
    }
}

Basic Catalog にネイティブ メディア レンダリングを統合する

提供されている Basic Catalog 実装(androidx.compose.material3:material3-a2ui)を使用する場合は、お好みのメディア ライブラリ(画像用の Coil や動画用の ExoPlayer など)を Basic Catalog のメディア コンポーネントに接続できます。

// Configure an Image component for the Basic Catalog using Coil
val coilImage = MaterialA2uiBasicCatalogV1Defaults.image { url, desc, scale, modifier, onError ->
    AsyncImage(
        model = url,
        contentDescription = desc,
        contentScale = scale,
        modifier = modifier,
        onError = { state -> onError(state.result.throwable) },
    )
}

実装の詳細

以降のセクションでは、再帰的な UI の出力、動的プロパティの評価、エラー報告について説明します。

コンポーネント実装のユーザー ジャーニーでは、次の主要な API を紹介します。

  • A2uiComponent: コンポーネントのメタデータ、プロパティ スキーマ、準備チェック(isReady)、レンダリング エミッション(Content)を定義するインターフェース。
  • A2uiProperty: JSON スキーマの生成とランタイム値の解決に使用される静的型付きプロパティ宣言。
  • A2uiComponentScope: コンポーネント実装にコンテキスト機能(データ バインディング、アクション ディスパッチ、子状態の監視など)を提供するレシーバ スコープ。
  • A2uiComponentProperties: 型安全なプロパティ アクセスを提供するエージェントから受け取ったコンポーネント プロパティのコンテナ。
  • A2uiComponentState: コンポーネントのリアクティブな読み込み、成功、エラー解決の状態を表します。

再帰的な UI の出力と動的ルーティング

呼び出し元によってホイストされたルート状態(または親内で解決された子コンポーネントの状態)は、A2uiComponent コンポーズ可能な関数を介して再帰的なコンポーネント レンダリングを開始します。この関数は、解決された状態を特定の UI 実装に密結合させるのではなく、動的ルーターとして機能します。