맞춤 A2UI 구성요소 구현

A2UI 아키텍처에서 모든 화면은 구성요소 카탈로그에 의해 구동됩니다. AI 에이전트가 자체 UI 기본 요소를 발명하거나 임의의 코드를 생성하는 대신 카탈로그에서 에이전트가 사용할 수 있는 구성요소, 속성 스키마, 기능을 선언합니다. 그러면 에이전트가 이러한 구성요소를 사용하여 사용자 인터페이스를 구성합니다.

앱의 디자인 시스템을 위해 맞춤 카탈로그를 빌드할 때 이러한 카탈로그 정의를 구체적인 Jetpack Compose UI 요소에 매핑하는 구성요소를 구현합니다. 각 A2UI 구성요소 (A2uiComponent)는 속성 스키마 계약을 정의하고, 동적 데이터가 도착하면 준비 상태를 평가하고, 데이터 모델에서 반응형 속성을 바인딩하고, Compose UI를 내보내고, 사용자 상호작용 작업을 에이전트로 다시 디스패치합니다.

Compose UI 렌더러 (androidx.a2ui.compose:compose-ui)는 앱의 디자인 시스템을 따르는 맞춤 구성요소를 구현하는 데 필요한 인터페이스와 수신기 범위를 제공합니다.

정적 유형 구성요소 속성 선언

렌더링하기 전에 구성요소가 에이전트로부터 예상하는 속성을 선언합니다. 런타임 레이어는 JSON 스키마 생성과 런타임에 값 추출에 모두 사용되는 정적으로 입력된 A2uiProperty API를 제공합니다.

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

일반 및 양방향 데이터 모델 바인딩 해결

구성요소 구현은 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) },
    )
}

구현 세부정보

다음 섹션에서는 재귀 UI 방출, 동적 속성 평가, 오류 보고를 설명합니다.

구성요소 구현 사용자 여정에서는 다음 주요 API를 소개합니다.

  • A2uiComponent: 구성요소 메타데이터, 속성 스키마, 준비 상태 확인 (isReady), 렌더링 방출 (Content)을 정의하는 인터페이스입니다.
  • A2uiProperty: JSON 스키마 생성 및 런타임 값 확인에 사용되는 정적으로 입력된 속성 선언입니다.
  • A2uiComponentScope: 컴포넌트 구현에 컨텍스트 기능(예: 데이터 결합, 작업 디스패치, 하위 상태 관찰)을 제공하는 수신기 범위입니다.
  • A2uiComponentProperties: 유형 안전 속성 액세스를 제공하는 에이전트로부터 수신된 구성요소 속성의 컨테이너입니다.
  • A2uiComponentState: 구성요소의 반응형 로드, 성공 또는 오류 해결 상태를 나타냅니다.

재귀적 UI 방출 및 동적 라우팅

호출자 (또는 상위 내에서 해결된 하위 구성요소 상태)에 의해 호이스팅된 루트 상태는 A2uiComponent 컴포저블 함수를 통해 재귀 구성요소 렌더링을 시작합니다. 이 함수는 확인된 상태를 특정 UI 구현에 긴밀하게 결합하는 대신 동적 라우터 역할을 합니다.