实现自定义 A2UI 组件

在 A2UI 架构中,每个界面都由组件目录驱动。您的目录会声明可供 AI 智能体使用的组件、属性架构和功能,而不是让 AI 智能体自行创建界面基元或生成任意代码。然后,代理会使用这些组件来构建用户界面。

为应用的设计体系构建自定义目录时,您需要实现一些组件,将这些目录定义映射到具体的 Jetpack Compose 界面元素。每个 A2UI 组件 (A2uiComponent) 都会定义其属性架构合约,在动态数据到达时评估就绪状态,绑定来自数据模型的响应式属性,发出 Compose 界面,并将用户互动操作调度回代理。

Compose 界面渲染器 (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 界面:

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) 来观察子元素状态。这样一来,父容器就可以在子组件独立加载时渲染其 shell,从而实现渐进式渲染:

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

在基本目录中集成原生媒体渲染

使用提供的基本目录实现 (androidx.compose.material3:material3-a2ui) 时,您可以将自己喜欢的媒体库(例如用于图片的 Coil 或用于视频的 ExoPlayer)插入到基本目录的媒体组件中:

// 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) },
    )
}

实现细节

以下部分将介绍递归界面发射、动态属性评估和错误报告。

组件实现用户历程介绍了以下关键 API:

  • A2uiComponent:用于定义组件元数据、属性架构、准备情况检查 (isReady) 和渲染发射 (Content) 的接口。
  • A2uiProperty:一种静态类型的属性声明,用于 JSON 架构生成和运行时值解析。
  • A2uiComponentScope:一种接收器范围,可为组件实现提供上下文功能(例如数据绑定、操作调度和子状态观察)。
  • A2uiComponentProperties:一个容器,用于存放从代理收到的组件属性,提供类型安全的属性访问。
  • A2uiComponentState:表示组件的响应式加载、成功或错误解决状态。

递归界面发射和动态路由

由调用方提升的根状态(或在父级中解析的子组件状态)通过 A2uiComponent 可组合函数启动递归组件渲染。此函数充当动态路由器,而不是将已解析的状态与特定的界面实现紧密耦合。