Implementar componentes personalizados da A2UI

Na arquitetura A2UI, cada superfície é impulsionada por um catálogo de componentes. Em vez de um agente de IA inventar suas próprias primitivas de interface ou gerar código arbitrário, seu catálogo declara os componentes, esquemas de propriedades e recursos disponíveis para o agente. Em seguida, o agente usa esses componentes para construir uma interface do usuário.

Ao criar um catálogo personalizado para o sistema de design do seu app, você implementa componentes que mapeiam essas definições do catálogo em elementos concretos da interface do Jetpack Compose. Cada componente da A2UI (A2uiComponent) define o contrato de esquema de propriedades, avalia a prontidão à medida que os dados dinâmicos chegam, vincula propriedades reativas do modelo de dados, emite a interface do Compose e envia ações de interação do usuário de volta ao agente.

O renderizador de interface do Compose (androidx.a2ui.compose:compose-ui) fornece as interfaces e os escopos de receptor necessários para implementar componentes personalizados que seguem o sistema de design do app.

Declarar propriedades de componentes com tipagem estática

Antes da renderização, declare as propriedades que um componente espera do agente. A camada de execução fornece APIs A2uiProperty com tipagem estática usadas para geração de esquema JSON e extração de valores no ambiente de execução:

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

Implementar a interface A2uiComponent

Implemente a interface A2uiComponent para definir o esquema de um componente e mapear propriedades recebidas do agente na interface do 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 vinculações de modelo de dados regulares e bidirecionais

As implementações de componentes usam A2uiComponentScope para resolver propriedades vinculadas dinamicamente. Para propriedades dinâmicas regulares, bind retorna o valor atual e se inscreve automaticamente nas atualizações do modelo de dados.

Para componentes de entrada interativos, bindUpdater retorna uma lambda de atualização estável. Se o agente forneceu uma string literal em vez de um caminho de dados gravável, a lambda do updater será null, indicando que o campo é somente leitura:

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

Enviar ações do usuário para o agente

Os componentes interativos usam A2uiComponentScope.dispatchAction para enviar eventos do usuário de volta ao 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)
            }
        }
    }
}

Processar componentes filhos e renderização progressiva

Os componentes que oferecem suporte a filhos aninhados usam observeA2uiComponentState(id) para observar os estados dos filhos. Isso permite a renderização progressiva, em que um contêiner pai renderiza o shell enquanto os componentes filhos são carregados de forma independente:

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 processar coleções ou listas de filhos (como itens em uma coluna, linha ou lista), declare uma propriedade usando A2uiProperty.childList e resolva os filhos com 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)
                }
            }
        }
    }
}

Integrar a renderização de mídia nativa no catálogo básico

Ao usar a implementação do catálogo básico fornecida (androidx.compose.material3:material3-a2ui), você pode conectar suas bibliotecas de mídia preferidas (como Coil para imagens ou ExoPlayer para vídeo) aos componentes de mídia do catálogo básico:

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

Detalhes de implementação

As seções a seguir explicam a emissão recursiva da interface, a avaliação dinâmica de propriedades e a geração de relatórios de erros.

As jornadas do usuário de implementação de componentes apresentam as seguintes APIs principais:

  • A2uiComponent: interface que define metadados de componentes, esquemas de propriedades, verificações de prontidão (isReady) e emissão de renderização (Content).
  • A2uiProperty: uma declaração de propriedade com tipo estático usada para geração de esquema JSON e resolução de valor de tempo de execução.
  • A2uiComponentScope: um escopo de receptor que fornece recursos contextuais (como vinculação de dados, envio de ações e observação do estado filho) para implementações de componentes.
  • A2uiComponentProperties: um contêiner para propriedades de componentes recebidas do agente que fornece acesso a propriedades com segurança de tipo.
  • A2uiComponentState: representa o estado de carregamento reativo, sucesso ou resolução de erro de um componente.

Emissão recursiva da interface e roteamento dinâmico

O estado raiz elevado pelo caller (ou estado do componente filho resolvido em um elemento pai) inicia a renderização recursiva de componentes pela função combinável A2uiComponent. Em vez de acoplar o estado resolvido a uma implementação de UI específica, essa função atua como um roteador dinâmico.