Implementa componenti A2UI personalizzati

Nell'architettura A2UI, ogni superficie è gestita da un catalogo di componenti. Anziché far inventare a un agente AI i propri primitivi della UI o generare codice arbitrario, il catalogo dichiara i componenti, gli schemi delle proprietà e le funzionalità disponibili per l'agente. L'agente utilizza questi componenti per costruire un'interfaccia utente.

Quando crei un catalogo personalizzato per il sistema di progettazione della tua app, implementi componenti che mappano queste definizioni di catalogo in elementi UI Jetpack Compose concreti. Ogni componente A2UI (A2uiComponent) definisce il proprio contratto di schema delle proprietà, valuta la preparazione all'arrivo dei dati dinamici, associa le proprietà reattive del modello di dati, emette l'UI di Compose e invia le azioni di interazione dell'utente all'agente.

Il renderer dell'interfaccia utente di Compose (androidx.a2ui.compose:compose-ui) fornisce le interfacce e gli ambiti del destinatario necessari per implementare componenti personalizzati, che seguono il sistema di progettazione della tua app.

Dichiarare le proprietà dei componenti con tipo statico

Prima del rendering, dichiara le proprietà che un componente si aspetta dall'agente. Il livello di runtime fornisce API A2uiProperty con tipizzazione statica utilizzate sia per la generazione dello schema JSON sia per l'estrazione dei valori in fase di runtime:

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

Implementare l'interfaccia A2uiComponent

Implementa l'interfaccia A2uiComponent per definire lo schema di un componente e mappare le proprietà ricevute dall'agente nell'UI di 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,
        )
    }
}

Risolvi i binding dei modelli di dati regolari e bidirezionali

Le implementazioni dei componenti utilizzano A2uiComponentScope per risolvere le proprietà associate dinamicamente. Per le proprietà dinamiche regolari, bind restituisce il valore corrente e si iscrive automaticamente agli aggiornamenti del modello di dati.

Per i componenti di input interattivi, bindUpdater restituisce una lambda di aggiornamento stabile. Se l'agente ha fornito una stringa letterale anziché un percorso dati scrivibile, la lambda dell'updater è null, a indicare che il campo è di sola lettura:

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

Invia le azioni dell'utente all'agente

I componenti interattivi utilizzano A2uiComponentScope.dispatchAction per inviare gli eventi utente all'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)
            }
        }
    }
}

Gestire i componenti secondari e il rendering progressivo

I componenti che supportano i figli nidificati utilizzano observeA2uiComponentState(id) per osservare gli stati dei figli. Ciò consente il rendering progressivo in cui un contenitore principale esegue il rendering della shell mentre i componenti secondari vengono caricati in modo indipendente:

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

Per gestire raccolte o elenchi di elementi secondari (ad esempio elementi in una colonna, una riga o un elenco), dichiara una proprietà utilizzando A2uiProperty.childList e risolvi gli elementi secondari 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 il rendering dei contenuti multimediali nativi nel catalogo di base

Quando utilizzi l'implementazione del catalogo di base fornita (androidx.compose.material3:material3-a2ui), puoi collegare le tue librerie multimediali preferite (come Coil per le immagini o ExoPlayer per i video) ai componenti multimediali del catalogo di base:

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

Dettagli di implementazione

Le sezioni seguenti spiegano l'emissione ricorsiva dell'interfaccia utente, la valutazione dinamica delle proprietà e la segnalazione degli errori.

I percorsi utente di implementazione dei componenti introducono le seguenti API chiave:

  • A2uiComponent: Interfaccia che definisce i metadati dei componenti, gli schemi delle proprietà, i controlli di idoneità (isReady) e l'emissione del rendering (Content).
  • A2uiProperty: una dichiarazione di proprietà con tipo statico utilizzata per la generazione dello schema JSON e la risoluzione dei valori di runtime.
  • A2uiComponentScope: un ambito del ricevitore che fornisce funzionalità contestuali (come l'associazione di dati, l'invio di azioni e l'osservazione dello stato secondario) alle implementazioni dei componenti.
  • A2uiComponentProperties: un contenitore per le proprietà dei componenti ricevute dall'agente che fornisce l'accesso alle proprietà con controllo del tipo.
  • A2uiComponentState: rappresenta lo stato di caricamento reattivo, riuscito o di risoluzione degli errori di un componente.

Emissione ricorsiva della UI e routing dinamico

Lo stato radice sollevato dal chiamante (o lo stato del componente figlio risolto all'interno di un componente padre) avvia il rendering ricorsivo dei componenti tramite la funzione composable A2uiComponent. Anziché accoppiare strettamente lo stato risolto a un'implementazione specifica dell'interfaccia utente, questa funzione funge da router dinamico.