Implementa componentes personalizados de A2UI

En la arquitectura de A2UI, cada superficie se basa en un catálogo de componentes. En lugar de que un agente de IA invente sus propias primitivas de IU o genere código arbitrario, tu catálogo declara los componentes, los esquemas de propiedades y las capacidades disponibles para el agente. Luego, el agente usa estos componentes para construir una interfaz de usuario.

Cuando compilas un catálogo personalizado para el sistema de diseño de tu app, implementas componentes que asignan esas definiciones del catálogo a elementos concretos de la IU de Jetpack Compose. Cada componente de A2UI (A2uiComponent) define su contrato de esquema de propiedades, evalúa la preparación a medida que llegan los datos dinámicos, vincula las propiedades reactivas del modelo de datos, emite la IU de Compose y envía las acciones de interacción del usuario al agente.

El renderizador de la IU de Compose (androidx.a2ui.compose:compose-ui) proporciona las interfaces y los ámbitos del receptor necesarios para implementar componentes personalizados que siguen el sistema de diseño de tu app.

Cómo declarar propiedades de componentes con escritura estática

Antes de renderizar, declara las propiedades que un componente espera del agente. La capa de tiempo de ejecución proporciona APIs A2uiProperty con escritura estática que se usan para la generación de esquemas JSON y la extracción de valores en el tiempo de ejecución:

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

Implementa la interfaz A2uiComponent

Implementa la interfaz A2uiComponent para definir el esquema de un componente y asignar las propiedades recibidas del agente a la IU de 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,
        )
    }
}

Resolver vinculaciones de modelos de datos regulares y bidireccionales

Las implementaciones de componentes usan A2uiComponentScope para resolver propiedades vinculadas de forma dinámica. Para las propiedades dinámicas normales, bind devuelve el valor actual y se suscribe automáticamente a las actualizaciones del modelo de datos.

En el caso de los componentes de entrada interactivos, bindUpdater devuelve una lambda de actualización estable. Si el agente proporcionó una cadena literal en lugar de una ruta de datos grabable, la función lambda del actualizador es null, lo que indica que el campo es de solo lectura:

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

Envía acciones del usuario al agente

Los componentes interactivos usan A2uiComponentScope.dispatchAction para enviar eventos del usuario al agente:

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

Cómo controlar los componentes secundarios y la renderización progresiva

Los componentes que admiten elementos secundarios anidados usan observeA2uiComponentState(id) para observar los estados de los elementos secundarios. Esto permite la renderización progresiva, en la que un contenedor principal renderiza su shell mientras los componentes secundarios se cargan de forma independiente:

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

Para controlar colecciones o listas de elementos secundarios (como elementos en una columna, una fila o una lista), declara una propiedad con A2uiProperty.childList y resuelve los elementos secundarios con 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)
                }
            }
        }
    }
}

Integra la renderización de medios nativos en el catálogo básico

Cuando usas la implementación de Basic Catalog proporcionada (androidx.compose.material3:material3-a2ui), puedes conectar tus bibliotecas de medios preferidas (como Coil para imágenes o ExoPlayer para videos) a los componentes de medios de 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) },
    )
}

Detalles de la implementación

En las siguientes secciones, se explican la emisión recursiva de la IU, la evaluación dinámica de propiedades y la generación de informes de errores.

Los recorridos del usuario de implementación de componentes presentan las siguientes APIs clave:

  • A2uiComponent: Es una interfaz que define los metadatos de los componentes, los esquemas de propiedades, las verificaciones de preparación (isReady) y la emisión de la renderización (Content).
  • A2uiProperty: Declaración de propiedad con escritura estática que se usa para la generación de esquemas JSON y la resolución de valores en el tiempo de ejecución.
  • A2uiComponentScope: Es un alcance del receptor que proporciona capacidades contextuales (como la vinculación de datos, el envío de acciones y la observación del estado secundario) a las implementaciones de componentes.
  • A2uiComponentProperties: Es un contenedor para las propiedades de los componentes que se reciben del agente y que proporciona acceso a las propiedades con seguridad de tipos.
  • A2uiComponentState: Representa el estado de resolución reactivo de carga, éxito o error de un componente.

Emisión recursiva de la IU y enrutamiento dinámico

El estado raíz que eleva el llamador (o el estado del componente secundario que se resuelve dentro de un elemento principal) inicia la renderización recursiva del componente a través de la función de componibilidad A2uiComponent. En lugar de vincular estrechamente el estado resuelto a una implementación de IU específica, esta función actúa como un enrutador dinámico.