Mengimplementasikan komponen A2UI kustom

Dalam arsitektur A2UI, setiap platform didorong oleh katalog komponen. Daripada membuat agen AI menciptakan primitif UI sendiri atau membuat kode arbitrer, katalog Anda mendeklarasikan komponen, skema properti, dan kemampuan yang tersedia untuk agen. Kemudian, agen menggunakan komponen ini untuk membuat antarmuka pengguna.

Saat membangun katalog kustom untuk sistem desain aplikasi, Anda menerapkan komponen yang memetakan definisi katalog tersebut ke elemen UI Jetpack Compose yang konkret. Setiap komponen A2UI (A2uiComponent) menentukan kontrak skema propertinya, mengevaluasi kesiapan saat data dinamis tiba, mengikat properti reaktif dari model data, memancarkan UI Compose, dan mengirimkan tindakan interaksi pengguna kembali ke agen.

Renderer UI Compose (androidx.a2ui.compose:compose-ui) menyediakan cakupan penerima dan antarmuka yang diperlukan untuk menerapkan komponen kustom, yang mengikuti sistem desain aplikasi Anda.

Mendeklarasikan properti komponen yang diketik secara statis

Sebelum merender, deklarasikan properti yang diharapkan komponen dari agen. Lapisan runtime menyediakan API A2uiProperty yang diketik secara statis yang digunakan untuk pembuatan skema JSON dan mengekstrak nilai saat 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)

Menerapkan antarmuka A2uiComponent

Terapkan antarmuka A2uiComponent untuk menentukan skema komponen dan memetakan properti yang diterima dari agen ke UI 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,
        )
    }
}

Menyelesaikan binding model data reguler dan dua arah

Implementasi komponen menggunakan A2uiComponentScope untuk menyelesaikan properti yang terikat secara dinamis. Untuk properti dinamis reguler, bind menampilkan nilai saat ini dan otomatis berlangganan pembaruan model data.

Untuk komponen input interaktif, bindUpdater menampilkan lambda updater yang stabil. Jika agen memberikan string literal, bukan jalur data yang dapat ditulis, lambda updater adalah null, yang menandakan bahwa kolom bersifat hanya baca:

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

Mengirim tindakan pengguna ke agen

Komponen interaktif menggunakan A2uiComponentScope.dispatchAction untuk mengirim peristiwa pengguna kembali ke agen:

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

Menangani komponen turunan dan rendering progresif

Komponen yang mendukung turunan bertingkat menggunakan observeA2uiComponentState(id) untuk mengamati status turunan. Hal ini memungkinkan rendering progresif di mana penampung induk merender shell-nya saat komponen turunan dimuat secara independen:

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

Untuk menangani kumpulan atau daftar turunan (seperti item dalam kolom, baris, atau daftar), deklarasikan properti menggunakan A2uiProperty.childList dan selesaikan turunan dengan 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)
                }
            }
        }
    }
}

Mengintegrasikan rendering media native di Katalog Dasar

Saat menggunakan penerapan Katalog Dasar yang disediakan (androidx.compose.material3:material3-a2ui), Anda dapat memasukkan library media pilihan Anda (seperti Coil untuk gambar atau ExoPlayer untuk video) ke komponen media Katalog Dasar:

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

Detail implementasi

Bagian berikut menjelaskan emisi UI rekursif, evaluasi properti dinamis, dan pelaporan error.

Perjalanan pengguna implementasi komponen memperkenalkan API utama berikut:

  • A2uiComponent: Antarmuka yang menentukan metadata komponen, skema properti, pemeriksaan kesiapan (isReady), dan emisi rendering (Content).
  • A2uiProperty: Deklarasi properti yang diketik secara statis yang digunakan untuk pembuatan skema JSON dan penyelesaian nilai runtime.
  • A2uiComponentScope: Cakupan penerima yang menyediakan kemampuan kontekstual (seperti pengikatan data, pengiriman tindakan, dan pengamatan status turunan) ke implementasi komponen.
  • A2uiComponentProperties: Penampung untuk properti komponen yang diterima dari agen yang menyediakan akses properti yang aman untuk jenis.
  • A2uiComponentState: Merepresentasikan status penyelesaian pemuatan reaktif, keberhasilan, atau error komponen.

Emisi UI rekursif dan perutean dinamis

Status root yang diangkat oleh pemanggil (atau status komponen turunan yang diselesaikan dalam komponen induk) memulai rendering komponen rekursif melalui fungsi composable A2uiComponent. Daripada menggabungkan status yang diselesaikan secara erat ke penerapan UI tertentu, fungsi ini bertindak sebagai perute dinamis.