Benutzerdefinierte A2UI-Komponenten implementieren

In der A2UI-Architektur wird jede Oberfläche von einem Komponentenkatalog gesteuert. Anstatt dass ein KI-Agent eigene UI-Grundelemente erfindet oder beliebigen Code generiert, werden in Ihrem Katalog die Komponenten, Eigenschaftsschemas und Funktionen deklariert, die dem Agenten zur Verfügung stehen. Der KI-Agent verwendet diese Komponenten dann, um eine Benutzeroberfläche zu erstellen.

Wenn Sie einen benutzerdefinierten Katalog für das Designsystem Ihrer App erstellen, implementieren Sie Komponenten, die diese Katalogdefinitionen konkreten Jetpack Compose-UI-Elementen zuordnen. Jede A2UI-Komponente (A2uiComponent) definiert ihren Eigenschaftsschema-Vertrag, bewertet die Bereitschaft, wenn dynamische Daten eingehen, bindet reaktive Eigenschaften aus dem Datenmodell, gibt die Compose-Benutzeroberfläche aus und sendet Aktionen zur Nutzerinteraktion zurück an den Agent.

Der Compose-UI-Renderer (androidx.a2ui.compose:compose-ui) bietet die Schnittstellen und Empfängerbereiche, die zum Implementieren benutzerdefinierter Komponenten erforderlich sind, die dem Designsystem Ihrer App entsprechen.

Statisch typisierte Komponenteneigenschaften deklarieren

Deklarieren Sie vor dem Rendern die Eigenschaften, die eine Komponente vom Agent erwartet. Die Laufzeitschicht bietet statisch typisierte A2uiProperty-APIs, die sowohl für die Generierung von JSON-Schemas als auch für das Extrahieren von Werten zur Laufzeit verwendet werden:

// 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-Schnittstelle implementieren

Implementieren Sie die A2uiComponent-Schnittstelle, um das Schema einer Komponente zu definieren und vom Agenten empfangene Eigenschaften der Compose-UI zuzuordnen:

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

Reguläre und bidirektionale Datenmodellbindungen auflösen

Bei der Implementierung von Komponenten wird A2uiComponentScope verwendet, um dynamisch gebundene Eigenschaften aufzulösen. Bei regulären dynamischen Eigenschaften gibt bind den aktuellen Wert zurück und abonniert automatisch Datenmodell-Updates.

Für interaktive Eingabekomponenten gibt bindUpdater eine stabile Updater-Lambda-Funktion zurück. Wenn der Agent einen Literalstring anstelle eines beschreibbaren Datenpfads bereitgestellt hat, ist die Updater-Lambda null, was darauf hinweist, dass das Feld schreibgeschützt ist:

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

Nutzeraktionen an den Agenten weiterleiten

Interaktive Komponenten verwenden A2uiComponentScope.dispatchAction, um Nutzerereignisse an den Agent zurückzusenden:

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

Untergeordnete Komponenten und progressives Rendern verarbeiten

Komponenten, die verschachtelte untergeordnete Elemente unterstützen, verwenden observeA2uiComponentState(id), um den Status untergeordneter Elemente zu beobachten. So kann das progressive Rendern ermöglicht werden, bei dem ein übergeordneter Container seine Shell rendert, während untergeordnete Komponenten unabhängig voneinander geladen werden:

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

Wenn Sie Sammlungen oder Listen von untergeordneten Elementen (z. B. Elemente in einer Spalte, Zeile oder Liste) verarbeiten möchten, deklarieren Sie eine Property mit A2uiProperty.childList und lösen Sie die untergeordneten Elemente mit bindChildReferences auf:

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

Native Media-Rendering in den Basic Catalog einbinden

Wenn Sie die bereitgestellte Basic Catalog-Implementierung (androidx.compose.material3:material3-a2ui) verwenden, können Sie Ihre bevorzugten Media-Bibliotheken (z. B. Coil für Bilder oder ExoPlayer für Videos) in die Media-Komponenten des Basic Catalog einfügen:

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

Details zur Implementierung

In den folgenden Abschnitten werden die rekursive UI-Ausgabe, die dynamische Attributauswertung und die Fehlerberichterstattung erläutert.

In den User Journeys für die Komponentenimplementierung werden die folgenden wichtigen APIs vorgestellt:

  • A2uiComponent: Schnittstelle zur Definition von Komponentenmetadaten, Attributschemata, Bereitschaftsprüfungen (isReady) und Rendering-Ausgabe (Content).
  • A2uiProperty: Eine statisch typisierte Eigenschaftendeklaration, die für die Generierung von JSON-Schemas und die Auflösung von Laufzeitwerten verwendet wird.
  • A2uiComponentScope: Ein Empfängerbereich, der Kontextfunktionen (z. B. Datenbindung, Aktionsversand und Beobachtung des untergeordneten Status) für die Komponentenimplementierungen bereitstellt.
  • A2uiComponentProperties: Ein Container für Komponenteneigenschaften, die vom Agent empfangen werden und einen typsicheren Zugriff auf Eigenschaften ermöglichen.
  • A2uiComponentState: Stellt den reaktiven Lade-, Erfolgs- oder Fehlerbehebungsstatus einer Komponente dar.

Rekursive UI-Ausgabe und dynamisches Routing

Der vom Aufrufer übergebene Root-Status (oder der in einem übergeordneten Element aufgelöste Status der untergeordneten Komponente) löst das rekursive Rendern der Komponente über die zusammensetzbare Funktion A2uiComponent aus. Anstatt den aufgelösten Status eng an eine bestimmte UI-Implementierung zu koppeln, fungiert diese Funktion als dynamischer Router.