實作自訂 A2UI 元件

在 A2UI 架構中,每個介面都是由元件目錄驅動。 目錄會向 AI 代理程式宣告可用的元件、屬性結構定義和功能,而不是讓 AI 代理程式自行發明 UI 基本元素或產生任意程式碼。然後,代理程式會使用這些元件建構使用者介面。

為應用程式的設計系統建構自訂目錄時,您會實作元件,將這些目錄定義對應至具體的 Jetpack Compose UI 元素。每個 A2UI 元件 (A2uiComponent) 都會定義自己的屬性結構定義合約、在動態資料抵達時評估就緒狀態、繫結資料模型中的反應式屬性、發出 Compose UI,以及將使用者互動動作傳回給代理程式。

Compose UI 算繪器 (androidx.a2ui.compose:compose-ui) 提供實作自訂元件所需的介面和接收器範圍,這些元件會遵循應用程式的設計系統。

宣告靜態型別元件屬性

在算繪之前,請先宣告元件預期從代理程式取得的屬性。執行階段層提供靜態型別的 A2uiProperty API,用於產生 JSON 結構定義,以及在執行階段擷取值:

// 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 會傳回穩定的更新程式 Lambda。 如果代理程式提供的是字串常值,而非可寫入的資料路徑,更新程式 Lambda 會是 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 實作項目。