בארכיטקטורת A2UI, כל משטח מונע על ידי קטלוג רכיבים. במקום שסוכן AI ימציא פרימיטיבים משלו לממשק משתמש או ייצור קוד שרירותי, הקטלוג שלכם מכריז על הרכיבים, סכימות המאפיינים והיכולות שזמינים לסוכן. הסוכן משתמש ברכיבים האלה כדי ליצור ממשק משתמש.
כשיוצרים קטלוג בהתאמה אישית למערכת העיצוב של האפליקציה, מטמיעים רכיבים שממפים את ההגדרות של הקטלוג לאלמנטים קונקרטיים של ממשק המשתמש ב-Jetpack Compose. כל רכיב A2UI (A2uiComponent) מגדיר את חוזה סכימת המאפיינים שלו, מעריך את המוכנות כשהנתונים הדינמיים מגיעים, מקשר מאפיינים תגובתיים ממודל הנתונים, שולח את ממשק המשתמש של Compose ושולח פעולות של אינטראקציה עם המשתמש בחזרה לסוכן.
רכיב העיבוד של ממשק המשתמש של 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 מחזירה lambda יציב לעדכון.
אם הסוכן סיפק מחרוזת מילולית במקום נתיב נתונים שניתן לכתיבה, פונקציית ה-lambda של העדכון היא 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. במקום לקשר באופן הדוק את המצב שנפתר ליישום ספציפי של ממשק המשתמש, הפונקציה הזו פועלת כנתב דינמי.