تنفيذ مكوّنات A2UI مخصّصة

في بنية A2UI، يتم تشغيل كل مساحة عرض من خلال كتالوج مكونات. بدلاً من أن يبتكر وكيل الذكاء الاصطناعي عناصر أساسية خاصة به في واجهة المستخدم أو ينشئ رمزًا برمجيًا عشوائيًا، يحدّد الكتالوج المكوّنات ومخططات الخصائص والإمكانات المتاحة للوكيل. يستخدم الوكيل بعد ذلك هذه المكوّنات لإنشاء واجهة مستخدم.

عند إنشاء كتالوج مخصّص لنظام تصميم تطبيقك، عليك تنفيذ المكوّنات التي تربط تعريفات الكتالوج هذه بعناصر واجهة مستخدم Jetpack Compose المحدّدة. يحدّد كل مكوّن من A2UI (A2uiComponent) عقد مخطط الخصائص، ويقيّم مدى الجاهزية عند وصول البيانات الديناميكية، ويربط الخصائص التفاعلية من نموذج البيانات، ويصدر واجهة مستخدم Compose، ويرسل إجراءات تفاعل المستخدم مرة أخرى إلى البرنامج.

يوفّر عارض واجهة مستخدم Compose (androidx.a2ui.compose:compose-ui) الواجهات ونطاقات المتلقّي اللازمة لتنفيذ مكوّنات مخصّصة تتّبع نظام تصميم تطبيقك.

تعريف خصائص المكوّنات ذات الأنواع الثابتة

قبل العرض، عليك تعريف الخصائص التي يتوقّع المكوّن تلقّيها من الوكيل. توفّر طبقة وقت التشغيل واجهات برمجة تطبيقات 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) },
    )
}

تفاصيل التنفيذ

توضّح الأقسام التالية عملية إنشاء واجهة مستخدم متكرّرة، وتقييم السمات الديناميكية، وإعداد تقارير الأخطاء.

تتضمّن رحلات المستخدمين لتنفيذ المكوّنات واجهات برمجة التطبيقات الرئيسية التالية:

  • ‫A2uiComponent: واجهة تحدّد البيانات الوصفية للمكوّن ومخططات الخصائص وعمليات التحقّق من الجاهزية (isReady) وإصدار العرض (Content).
  • ‫A2uiProperty: بيان خاصية مكتوب بشكل ثابت ويُستخدم لإنشاء مخطط JSON وحلّ قيمة وقت التشغيل.
  • A2uiComponentScope: نطاق مستقبِل يوفّر إمكانات سياقية (مثل ربط البيانات وإرسال الإجراءات ومراقبة حالة العنصر التابع) لعمليات تنفيذ المكوّنات.
  • ‫A2uiComponentProperties: حاوية لسمات المكوّن التي تم تلقّيها من الوكيل الذي يوفّر إمكانية الوصول إلى السمات الآمنة من حيث النوع.
  • ‫A2uiComponentState: تمثّل حالة التحميل التفاعلي أو النجاح أو حلّ الخطأ لأحد المكوّنات.

إصدار واجهة مستخدم متكرّرة وتوجيه ديناميكي

تبدأ عملية عرض المكوّن المتكرّر من خلال الدالة المركّبة A2uiComponent، وذلك باستخدام حالة الجذر التي يتم نقلها من خلال المتصل (أو حالة المكوّن الفرعي التي يتم حلّها داخل مكوّن رئيسي). وبدلاً من الربط الوثيق للحالة التي تم حلّها بتنفيذ معيّن لواجهة المستخدم، تعمل هذه الدالة كجهاز توجيه ديناميكي.