Triển khai các thành phần A2UI tuỳ chỉnh

Trong cấu trúc A2UI, mọi thành phần đều được điều khiển bằng một danh mục thành phần. Thay vì để một tác nhân AI tự tạo các thành phần giao diện người dùng riêng hoặc tạo mã tuỳ ý, danh mục của bạn sẽ khai báo các thành phần, giản đồ thuộc tính và khả năng mà tác nhân có thể sử dụng. Sau đó, tác nhân sẽ sử dụng các thành phần này để tạo giao diện người dùng.

Khi tạo một danh mục tuỳ chỉnh cho hệ thống thiết kế của ứng dụng, bạn sẽ triển khai các thành phần ánh xạ những định nghĩa danh mục đó thành các phần tử giao diện người dùng Jetpack Compose cụ thể. Mỗi thành phần A2UI (A2uiComponent) xác định hợp đồng lược đồ thuộc tính, đánh giá trạng thái sẵn sàng khi dữ liệu động đến, liên kết các thuộc tính phản ứng từ mô hình dữ liệu, phát ra giao diện người dùng Compose và gửi các thao tác tương tác của người dùng trở lại tác nhân.

Trình kết xuất giao diện người dùng Compose (androidx.a2ui.compose:compose-ui) cung cấp các giao diện và phạm vi của receiver cần thiết để triển khai các thành phần tuỳ chỉnh, tuân theo hệ thống thiết kế của ứng dụng.

Khai báo các thuộc tính thành phần được nhập tĩnh

Trước khi hiển thị, hãy khai báo các thuộc tính mà một thành phần mong đợi từ tác nhân. Lớp thời gian chạy cung cấp các API A2uiProperty được nhập tĩnh dùng cho cả việc tạo giản đồ JSON và trích xuất các giá trị tại thời gian chạy:

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

Triển khai giao diện A2uiComponent

Triển khai giao diện A2uiComponent để xác định giản đồ của một thành phần và ánh xạ các thuộc tính nhận được từ tác nhân lên giao diện người dùng 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,
        )
    }
}

Giải quyết các liên kết mô hình dữ liệu thông thường và hai chiều

Các phương thức triển khai thành phần sử dụng A2uiComponentScope để phân giải các thuộc tính được liên kết động. Đối với các thuộc tính động thông thường, bind sẽ trả về giá trị hiện tại và tự động đăng ký cập nhật mô hình dữ liệu.

Đối với các thành phần đầu vào tương tác, bindUpdater sẽ trả về một hàm cập nhật lambda ổn định. Nếu tác nhân cung cấp một chuỗi ký tự thay vì một đường dẫn dữ liệu có thể ghi, thì lambda của trình cập nhật là null, báo hiệu rằng trường này chỉ có thể đọc:

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

Gửi thao tác của người dùng đến tác nhân

Các thành phần tương tác sử dụng A2uiComponentScope.dispatchAction để gửi các sự kiện của người dùng trở lại cho tác nhân:

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

Xử lý các thành phần con và kết xuất tăng dần

Các thành phần hỗ trợ thành phần con lồng nhau sử dụng observeA2uiComponentState(id) để theo dõi trạng thái của thành phần con. Điều này cho phép kết xuất tăng dần, trong đó một vùng chứa mẹ kết xuất lớp vỏ trong khi các thành phần con tải độc lập:

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

Để xử lý các tập hợp hoặc danh sách phần tử con (chẳng hạn như các mục trong cột, hàng hoặc danh sách), hãy khai báo một thuộc tính bằng cách sử dụng A2uiProperty.childList và giải quyết các phần tử con bằng 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)
                }
            }
        }
    }
}

Tích hợp tính năng kết xuất nội dung nghe nhìn gốc trong Danh mục cơ bản

Khi sử dụng chế độ triển khai Danh mục cơ bản được cung cấp (androidx.compose.material3:material3-a2ui), bạn có thể cắm các thư viện nội dung nghe nhìn mà mình muốn (chẳng hạn như Coil cho hình ảnh hoặc ExoPlayer cho video) vào các thành phần nội dung nghe nhìn của Danh mục cơ bản:

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

Thông tin chi tiết về việc triển khai

Các phần sau đây giải thích về việc phát ra giao diện người dùng đệ quy, đánh giá thuộc tính động và báo cáo lỗi.

Hành trình của người dùng khi triển khai thành phần giới thiệu các API chính sau đây:

  • A2uiComponent: Giao diện xác định siêu dữ liệu thành phần, giản đồ thuộc tính, quy trình kiểm tra trạng thái sẵn sàng (isReady) và quá trình phát hành kết xuất (Content).
  • A2uiProperty: Một khai báo thuộc tính được nhập tĩnh dùng để tạo giản đồ JSON và phân giải giá trị thời gian chạy.
  • A2uiComponentScope: Một phạm vi của receiver cung cấp các chức năng theo bối cảnh (chẳng hạn như liên kết dữ liệu, gửi hành động và theo dõi trạng thái của thành phần con) cho việc triển khai thành phần.
  • A2uiComponentProperties: Một vùng chứa cho các thuộc tính thành phần nhận được từ tác nhân cung cấp quyền truy cập vào thuộc tính an toàn về kiểu.
  • A2uiComponentState: Biểu thị trạng thái phân giải lỗi, thành công hoặc tải phản ứng của một thành phần.

Phát xạ giao diện người dùng đệ quy và định tuyến linh động

State gốc do phương thức gọi chuyển lên trên (hoặc state thành phần con được phân giải trong một thành phần mẹ) sẽ bắt đầu quá trình kết xuất thành phần đệ quy thông qua hàm composable A2uiComponent. Thay vì liên kết chặt chẽ trạng thái đã phân giải với một cách triển khai giao diện người dùng cụ thể, hàm này hoạt động như một bộ định tuyến động.