Wdrażanie niestandardowych komponentów A2UI

W architekturze A2UI każda powierzchnia jest obsługiwana przez katalog komponentów. Zamiast pozwalać agentowi AI na wymyślanie własnych elementów interfejsu lub generowanie dowolnego kodu, katalog deklaruje komponenty, schematy właściwości i funkcje dostępne dla agenta. Następnie agent używa tych komponentów do tworzenia interfejsu użytkownika.

Gdy tworzysz katalog niestandardowy dla systemu projektowania aplikacji, implementujesz komponenty, które mapują definicje katalogu na konkretne elementy interfejsu Jetpack Compose. Każdy komponent A2UI (A2uiComponent) definiuje kontrakt schematu właściwości, ocenia gotowość w miarę napływu danych dynamicznych, wiąże właściwości reaktywne z modelu danych, emituje interfejs Compose i wysyła działania związane z interakcją użytkownika z powrotem do agenta.

Renderowanie interfejsu Compose (androidx.a2ui.compose:compose-ui) udostępnia interfejsy i zakresy odbiorników potrzebne do implementowania komponentów niestandardowych, które są zgodne z systemem projektowania aplikacji.

Deklarowanie właściwości komponentu o statycznym typie

Przed renderowaniem zadeklaruj właściwości, których komponent oczekuje od agenta. Warstwa środowiska wykonawczego udostępnia statycznie typowane interfejsy API A2uiProperty, które są używane zarówno do generowania schematu JSON, jak i do wyodrębniania wartości w czasie działania:

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

Implementowanie interfejsu A2uiComponent

Zaimplementuj interfejs A2uiComponent, aby zdefiniować schemat komponentu i zmapować właściwości otrzymane od agenta na interfejs 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,
        )
    }
}

Rozwiązywanie regularnych i dwukierunkowych powiązań modelu danych

Implementacje komponentów używają A2uiComponentScope do rozwiązywania dynamicznie powiązanych właściwości. W przypadku zwykłych właściwości dynamicznych funkcja bind zwraca bieżącą wartość i automatycznie subskrybuje aktualizacje modelu danych.

W przypadku interaktywnych komponentów wejściowych funkcja bindUpdater zwraca stabilną lambdę aktualizatora. Jeśli agent podał ciąg dosłowny zamiast ścieżki danych z możliwością zapisu, funkcja lambda aktualizatora ma wartość null, co oznacza, że pole jest tylko do odczytu:

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

Przekazywanie działań użytkownika do agenta

Komponenty interaktywne używają A2uiComponentScope.dispatchAction do wysyłania zdarzeń użytkownika z powrotem do agenta:

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

Obsługa komponentów podrzędnych i progresywne renderowanie

Komponenty obsługujące zagnieżdżone elementy podrzędne używają observeA2uiComponentState(id) do obserwowania stanów elementów podrzędnych. Umożliwia to progresywne renderowanie, w którym kontener nadrzędny renderuje swoją powłokę, a komponenty podrzędne wczytują się niezależnie:

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

Aby obsługiwać kolekcje lub listy elementów podrzędnych (np. elementy w kolumnie, wierszu lub na liście), zadeklaruj właściwość za pomocą A2uiProperty.childList i rozwiąż elementy podrzędne za pomocą 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)
                }
            }
        }
    }
}

Integracja renderowania multimediów natywnych w katalogu podstawowym

Korzystając z podanej implementacji katalogu podstawowego (androidx.compose.material3:material3-a2ui), możesz podłączyć do komponentów multimedialnych katalogu podstawowego preferowane biblioteki multimediów (np. Coil do obrazów lub ExoPlayer do filmów):

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

Szczegóły implementacji

W kolejnych sekcjach wyjaśniamy rekurencyjne emitowanie interfejsu, dynamiczną ocenę właściwości i raportowanie błędów.

W ramach ścieżek użytkownika implementacji komponentów przedstawiamy te kluczowe interfejsy API:

  • A2uiComponent: interfejs definiujący metadane komponentu, schematy właściwości, testy gotowości (isReady) i emitowanie renderowania (Content).
  • A2uiProperty: deklaracja właściwości o statycznym typie używana do generowania schematu JSON i rozwiązywania wartości w czasie działania.
  • A2uiComponentScope: Zakres odbiornika zapewniający możliwości kontekstowe (takie jak powiązanie danych, wysyłanie działań i obserwowanie stanu podrzędnego) implementacjom komponentów.
  • A2uiComponentProperties: kontener właściwości komponentu otrzymanych od agenta, który zapewnia dostęp do właściwości z zachowaniem bezpieczeństwa typów.
  • A2uiComponentState: reprezentuje stan reaktywnego wczytywania, powodzenia lub błędu komponentu.

Rekursywne emitowanie interfejsu i routing dynamiczny

Stan główny podniesiony przez element wywołujący (lub stan komponentu podrzędnego rozwiązany w komponencie nadrzędnym) rozpoczyna rekurencyjne renderowanie komponentu za pomocą funkcji typu „composable” A2uiComponent. Zamiast ściśle wiązać stan rozwiązania z konkretną implementacją interfejsu, ta funkcja działa jak dynamiczny router.