在 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 可组合函数启动递归组件渲染。此函数充当动态路由器,而不是将已解析的状态与特定的界面实现紧密耦合。