Реализуйте пользовательские компоненты A2UI.

В архитектуре A2UI каждый интерфейс управляется каталогом компонентов . Вместо того чтобы ИИ-агент изобретал собственные примитивы пользовательского интерфейса или генерировал произвольный код, ваш каталог объявляет компоненты, схемы свойств и возможности, доступные агенту. Затем агент использует эти компоненты для построения пользовательского интерфейса.

При создании пользовательского каталога для системы дизайна вашего приложения вы реализуете компоненты, которые сопоставляют эти определения каталога с конкретными элементами пользовательского интерфейса Jetpack Compose. Каждый компонент A2UI ( A2uiComponent ) определяет свой контракт схемы свойств, оценивает готовность по мере поступления динамических данных, связывает реактивные свойства из модели данных, генерирует Compose UI и отправляет действия взаимодействия пользователя обратно агенту.

Рендерер пользовательского интерфейса Compose ( androidx.a2ui.compose:compose-ui ) предоставляет интерфейсы и области видимости, необходимые для реализации пользовательских компонентов, соответствующих системе дизайна вашего приложения.

Объявить статически типизированные свойства компонента

Перед рендерингом необходимо объявить свойства, которые компонент ожидает от агента. Слой выполнения предоставляет статически типизированные API A2uiProperty , используемые как для генерации JSON-схемы, так и для извлечения значений во время выполнения:

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

Реализуйте интерфейс A2uiComponent для определения схемы компонента и сопоставления свойств, полученных от агента, с пользовательским интерфейсом 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,
        )
    }
}

Разрешение обычных и двусторонних привязок модели данных.

В реализациях компонентов для разрешения динамически связанных свойств используется A2uiComponentScope . Для обычных динамических свойств bind возвращает текущее значение и автоматически подписывается на обновления модели данных.

Для интерактивных компонентов ввода bindUpdater возвращает стабильную лямбда-функцию обновления. Если агент предоставил строковый литерал вместо пути к данным, доступным для записи, лямбда-функция обновления будет null , что указывает на то, что поле доступно только для чтения:

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

Передайте действия пользователя агенту.

Интерактивные компоненты используют A2uiComponentScope.dispatchAction для отправки событий пользователя обратно агенту:

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

Обработка дочерних компонентов и прогрессивной отрисовки.

Компоненты, поддерживающие вложенные дочерние элементы, используют observeA2uiComponentState(id) для отслеживания состояний дочерних элементов. Это позволяет осуществлять прогрессивную отрисовку, при которой родительский контейнер отображает свою оболочку, в то время как дочерние компоненты загружаются независимо:

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

Для работы с коллекциями или списками дочерних элементов (например, элементами в столбце, строке или списке) объявите свойство с помощью A2uiProperty.childList и разрешите дочерние элементы с помощью 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)
                }
            }
        }
    }
}

Интегрировать встроенную функцию рендеринга мультимедиа в базовый каталог.

При использовании предоставленной реализации базового каталога ( androidx.compose.material3:material3-a2ui ) вы можете подключать предпочитаемые медиатеки (например, Coil для изображений или ExoPlayer для видео) к медиакомпонентам базового каталога:

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

Детали реализации

В следующих разделах объясняется рекурсивная генерация элементов пользовательского интерфейса, динамическая оценка свойств и отчеты об ошибках.

В сценариях взаимодействия пользователей с компонентами представлены следующие ключевые API:

  • A2uiComponent : Интерфейс, определяющий метаданные компонента, схемы свойств, проверки готовности ( isReady ) и отображение содержимого ( Content ).
  • A2uiProperty : объявление статически типизированного свойства, используемое для генерации JSON-схемы и разрешения значений во время выполнения.
  • A2uiComponentScope : Область видимости приемника, предоставляющая контекстные возможности (такие как привязка данных, диспетчеризация действий и наблюдение за состоянием дочерних элементов) реализациям компонентов.
  • A2uiComponentProperties : Контейнер для свойств компонентов, получаемых от агента, обеспечивающий типобезопасный доступ к свойствам.
  • A2uiComponentState : Представляет состояние реактивной загрузки, успешного выполнения или разрешения ошибки компонента.

Рекурсивное создание пользовательского интерфейса и динамическая маршрутизация

Состояние корневого компонента, переданное вызывающей стороной (или состояние дочернего компонента, разрешенное внутри родительского компонента), запускает рекурсивную отрисовку компонента через компонуемую функцию A2uiComponent . Вместо того чтобы жестко привязывать разрешенное состояние к конкретной реализации пользовательского интерфейса, эта функция действует как динамический маршрутизатор.