Implémenter des composants A2UI personnalisés

Dans l'architecture A2UI, chaque surface est pilotée par un catalogue de composants. Plutôt que de laisser un agent d'IA inventer ses propres primitives d'UI ou générer du code arbitraire, votre catalogue déclare les composants, les schémas de propriétés et les capacités disponibles pour l'agent. L'agent utilise ensuite ces composants pour construire une interface utilisateur.

Lorsque vous créez un catalogue personnalisé pour le système de conception de votre application, vous implémentez des composants qui mappent ces définitions de catalogue dans des éléments d'UI Jetpack Compose concrets. Chaque composant A2UI (A2uiComponent) définit son contrat de schéma de propriété, évalue l'état de préparation à l'arrivée des données dynamiques, lie les propriétés réactives du modèle de données, émet l'UI Compose et renvoie les actions d'interaction utilisateur à l'agent.

Le moteur de rendu de l'interface utilisateur Compose (androidx.a2ui.compose:compose-ui) fournit les interfaces et les portées du récepteur nécessaires à l'implémentation de composants personnalisés, qui suivent le système de conception de votre application.

Déclarer les propriétés des composants typés de manière statique

Avant le rendu, déclarez les propriétés qu'un composant attend de l'agent. La couche d'exécution fournit des API A2uiProperty à typage statique utilisées à la fois pour la génération de schémas JSON et pour l'extraction de valeurs au moment de l'exécution :

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

Implémenter l'interface A2uiComponent

Implémentez l'interface A2uiComponent pour définir le schéma d'un composant et mapper les propriétés reçues de l'agent sur Compose UI :

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

Résoudre les liaisons de modèle de données régulières et bidirectionnelles

Les implémentations de composants utilisent A2uiComponentScope pour résoudre les propriétés liées dynamiquement. Pour les propriétés dynamiques régulières, bind renvoie la valeur actuelle et s'abonne automatiquement aux mises à jour du modèle de données.

Pour les composants d'entrée interactifs, bindUpdater renvoie un lambda de mise à jour stable. Si l'agent a fourni une chaîne littérale au lieu d'un chemin d'accès aux données modifiable, le lambda de mise à jour est null, ce qui indique que le champ est en lecture seule :

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

Envoyer les actions de l'utilisateur à l'agent

Les composants interactifs utilisent A2uiComponentScope.dispatchAction pour renvoyer les événements utilisateur à l'agent :

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

Gérer les composants enfants et le rendu progressif

Les composants acceptant les enfants imbriqués utilisent observeA2uiComponentState(id) pour observer les états des enfants. Cela permet un rendu progressif où un conteneur parent affiche son shell pendant que les composants enfants se chargent indépendamment :

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

Pour gérer des collections ou des listes d'enfants (tels que des éléments dans une colonne, une ligne ou une liste), déclarez une propriété à l'aide de A2uiProperty.childList et résolvez les enfants avec 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)
                }
            }
        }
    }
}

Intégrer le rendu des contenus multimédias natifs dans le catalogue de base

Lorsque vous utilisez l'implémentation de catalogue de base fournie (androidx.compose.material3:material3-a2ui), vous pouvez brancher vos bibliothèques multimédias préférées (telles que Coil pour les images ou ExoPlayer pour les vidéos) dans les composants multimédias du catalogue de 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) },
    )
}

Détails de mise en œuvre

Les sections suivantes expliquent l'émission récursive d'UI, l'évaluation dynamique des propriétés et le signalement des erreurs.

Les parcours utilisateur d'implémentation de composants présentent les API clés suivantes :

  • A2uiComponent : interface définissant les métadonnées des composants, les schémas de propriétés, les vérifications de disponibilité (isReady) et l'émission de rendu (Content).
  • A2uiProperty : déclaration de propriété à typage statique utilisée pour la génération de schémas JSON et la résolution des valeurs d'exécution.
  • A2uiComponentScope : portée du récepteur fournissant des fonctionnalités contextuelles (telles que la liaison de données, l'envoi d'actions et l'observation de l'état enfant) aux implémentations de composants.
  • A2uiComponentProperties : conteneur pour les propriétés de composants reçues de l'agent qui fournit un accès aux propriétés avec sécurité du type.
  • A2uiComponentState : représente l'état de chargement réactif, de réussite ou d'erreur d'un composant.

Émission récursive de l'UI et routage dynamique

L'état racine hissé par l'appelant (ou l'état du composant enfant résolu dans un parent) déclenche le rendu récursif des composants via la fonction composable A2uiComponent. Plutôt que de coupler étroitement l'état résolu à une implémentation d'UI spécifique, cette fonction agit comme un routeur dynamique.