اعتبارات أخرى

على الرغم من أنّ عملية نقل البيانات من Views إلى Compose تتعلّق بواجهة المستخدم فقط، هناك الكثير من الأمور التي يجب أخذها في الاعتبار لإجراء عملية نقل آمنة وتدريجية. تحتوي هذه الصفحة على بعض الاعتبارات التي يجب مراعاتها أثناء نقل تطبيقك المستند إلى View إلى Compose.

نقل مظهر تطبيقك

‫التصميم المتعدد الأبعاد هو نظام التصميم المقترَح لتطبيق السمات على تطبيقات Android.

بالنسبة إلى التطبيقات المستندة إلى View، تتوفّر ثلاثة إصدارات من Material:

  • الإصدار الأول من التصميم المتعدد الأبعاد باستخدام مكتبة AppCompat (أي Theme.AppCompat.*)
  • الإصدار 2 من التصميم المتعدد الأبعاد باستخدام مكتبة MDC-Android (أي Theme.MaterialComponents.*)
  • ‫التصميم المتعدد الأبعاد 3 باستخدام مكتبة MDC-Android (أي Theme.Material3.*)

بالنسبة إلى تطبيقات Compose، يتوفّر إصداران من Material:

  • ‫التصميم المتعدد الأبعاد 2 باستخدام مكتبة Compose Material (أي androidx.compose.material.MaterialTheme)
  • ‫Material Design 3 باستخدام مكتبة Compose Material 3 (أي androidx.compose.material3.MaterialTheme)

ننصحك باستخدام أحدث إصدار (Material 3) إذا كان نظام تصميم تطبيقك يسمح بذلك. تتوفّر أدلة نقل البيانات لكل من Views وCompose:

عند إنشاء شاشات جديدة في Compose، بغض النظر عن إصدار التصميم المتعدد الأبعاد المستخدَم، تأكَّد من تطبيق MaterialTheme قبل أي دوال مركّبة تُصدر واجهة مستخدم من مكتبات Compose Material. تعتمد مكوّنات Material (Button وText وما إلى ذلك) على توفّر MaterialTheme ولا يمكن تحديد سلوكها بدونه.

تستخدم جميع نماذج Jetpack Compose مظهرًا مخصّصًا من Compose تم إنشاؤه على MaterialTheme.

لمزيد من المعلومات، يُرجى الاطّلاع على أنظمة التصميم في Compose ونقل مظاهر XML إلى Compose.

إذا كنت تستخدم مكوّن التنقّل في تطبيقك، يمكنك الاطّلاع على التنقّل من Compose في تطبيق مستند إلى الأجزاء ونقل Jetpack Navigation إلى Navigation Compose لمزيد من المعلومات.

اختبار واجهة مستخدم Compose/Views المختلطة

بعد نقل أجزاء من تطبيقك إلى Compose، من المهم إجراء الاختبارات للتأكّد من عدم حدوث أي مشاكل.

عندما يستخدم نشاط أو جزء Compose، عليك استخدام createAndroidComposeRule بدلاً من ActivityScenarioRule. تدمج أداة createAndroidComposeRule بين ActivityScenarioRule وComposeTestRule تتيح لك اختبار Compose وعرض الرمز البرمجي في الوقت نفسه.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

لمزيد من المعلومات حول الاختبار، يُرجى الاطّلاع على اختبار تنسيق Compose. لتحقيق إمكانية التشغيل التفاعلي مع أُطر عمل اختبار واجهة المستخدم، يُرجى الاطّلاع على إمكانية التشغيل التفاعلي مع Espresso وإمكانية التشغيل التفاعلي مع UiAutomator.

دمج Compose مع بنية تطبيقك الحالية

Unidirectional Data Flow (UDF) architecture patterns work seamlessly with Compose. إذا كان التطبيق يستخدم أنواعًا أخرى من أنماط البنية بدلاً من ذلك، مثل Model View Presenter (MVP)، ننصحك بنقل هذا الجزء من واجهة المستخدم إلى UDF قبل استخدام Compose أو أثناءه.

استخدام ViewModel في ميزة "الإنشاء"

إذا كنت تستخدم مكتبة مكوّنات البنية ViewModel، يمكنك الوصول إلى ViewModel من أي دالة مركّبة من خلال استدعاء الدالة viewModel() ، كما هو موضّح في Compose والمكتبات الأخرى.

عند استخدام Compose، يجب توخي الحذر بشأن استخدام نوع ViewModel نفسه في دوال مركّبة مختلفة لأنّ عناصر ViewModel تتّبع نطاقات مراحل نشاط View. سيكون النطاق إما نشاط المضيف أو الجزء أو الرسم البياني للتنقّل في حال استخدام مكتبة التنقّل.

على سبيل المثال، إذا تمت استضافة الدوال المركّبة في نشاط، ستعرض دالة viewModel() دائمًا المثيل نفسه الذي يتم محوه فقط عند انتهاء النشاط. في المثال التالي، يتم الترحيب بالمستخدم نفسه ("user1") مرتين لأنّه تتم إعادة استخدام مثيل GreetingViewModel نفسه في جميع الدوال المركّبة ضمن النشاط المضيف. تتم إعادة استخدام أول مثيل ViewModel تم إنشاؤه في دوال مركّبة أخرى.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

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

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

المرجع الموثوق للولاية

عند استخدام Compose في جزء واحد من واجهة المستخدم، من الممكن أن يحتاج Compose ورمز نظام العرض إلى مشاركة البيانات. ننصحك، إن أمكن، بتضمين تلك الحالة المشتركة في فئة أخرى تتبع أفضل ممارسات تدفق البيانات أحادي الاتجاه (UDF) التي تستخدمها كلتا المنصتين، على سبيل المثال، في ViewModel يعرض مجموعة من البيانات المشتركة لإصدار تحديثات البيانات.

ومع ذلك، لا يكون ذلك ممكنًا دائمًا إذا كانت البيانات المطلوب مشاركتها قابلة للتغيير أو مرتبطة ارتباطًا وثيقًا بعنصر في واجهة المستخدم. في هذه الحالة، يجب أن يكون أحد النظامين هو المصدر الموثوق، ويجب أن يشارك هذا النظام أي تحديثات للبيانات مع النظام الآخر. كقاعدة عامة، يجب أن يكون مصدر المعلومات مملوكًا لأي عنصر أقرب إلى جذر التسلسل الهرمي لواجهة المستخدم.

‫Compose كمصدر موثوق

استخدِم الدالة البرمجية المركّبة SideEffect لنشر حالة Compose إلى رمز غير تابع لـ Compose. في هذه الحالة، يتم الاحتفاظ بالمصدر الموثوق في دالة مركّبة ترسل تحديثات الحالة.

على سبيل المثال، قد تسمح لك مكتبة الإحصاءات بتقسيم مجموعة المستخدمين من خلال إرفاق بيانات وصفية مخصّصة (خصائص المستخدمين في هذا المثال) بجميع أحداث الإحصاءات اللاحقة. لإبلاغ مكتبة الإحصاءات بنوع المستخدم الحالي، استخدِم SideEffect لتعديل قيمته.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

لمزيد من المعلومات، يُرجى الاطّلاع على التأثيرات الجانبية في Compose.

عرض النظام كمصدر موثوق به

إذا كان نظام العرض يمتلك الحالة ويشاركها مع Compose، ننصحك بتضمين الحالة في كائنات mutableStateOf لجعلها آمنة لسلسلة التعليمات في Compose. إذا استخدمت هذا الأسلوب، يتم تبسيط الدوال المركّبة لأنّها لم تعُد تحتوي على المصدر الموثوق، ولكن يحتاج نظام العرض إلى تعديل الحالة القابلة للتغيير وطرق العرض التي تستخدم هذه الحالة.

في المثال التالي، يحتوي CustomViewGroup على TextView وComposeView مع دالة مركّبة TextField بداخله. يجب أن يعرض TextView محتوى ما يكتبه المستخدم في TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

نقل واجهة المستخدم المشتركة

إذا كنت تنتقل تدريجيًا إلى Compose، قد تحتاج إلى استخدام عناصر واجهة مستخدم مشتركة في كل من Compose ونظام العرض. على سبيل المثال، إذا كان تطبيقك يحتوي على مكوّن CallToActionButton مخصّص، قد تحتاج إلى استخدامه في كل من الشاشات المستندة إلى Compose والشاشات المستندة إلى View.

في Compose، تصبح عناصر واجهة المستخدم المشتركة دوال مركّبة يمكن إعادة استخدامها في جميع أنحاء التطبيق، بغض النظر عن العنصر الذي يتم تصميمه باستخدام XML أو كونه طريقة عرض مخصّصة. على سبيل المثال، يمكنك إنشاء دالة مركّبة CallToActionButton لمكوّن عبارة الحث على اتخاذ إجراء Button المخصّص.

لاستخدام الدالة المركّبة في الشاشات المستندة إلى العرض، أنشئ برنامج تضمين مخصّصًا للعرض يمتد من AbstractComposeView. في الدالة المركّبة Content التي تم إلغاؤها، ضَع الدالة المركّبة التي أنشأتها داخل مظهر Compose، كما هو موضّح في المثال أدناه:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

لاحظ أن معلمات الدالة المركّبة تصبح متغيرات قابلة للتغيير داخل العرض المخصص. هذا يجعل العرض المخصص CallToActionViewButton قابلاً للتضخم والاستخدام، مثل العرض التقليدي. يمكنك الاطّلاع على مثال على ذلك باستخدام ربط العرض أدناه:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

إذا كان المكوّن المخصّص يحتوي على حالة قابلة للتغيير، يُرجى الاطّلاع على مصدر الحالة الموثوق به.

إعطاء الأولوية لفصل الحالة عن العرض

عادةً ما يكون View ذو حالة. يدير View الحقول التي تصف ما يجب عرضه، بالإضافة إلى كيفية عرضه. عند تحويل View إلى Compose، عليك فصل البيانات التي يتم عرضها لتحقيق تدفق بيانات أحادي الاتجاه، كما هو موضّح في نقل القيمة.

على سبيل المثال، يحتوي View على سمة visibility تصف ما إذا كان مرئيًا أو غير مرئي أو مخفيًا. هذه سمة متأصلة في View. While other pieces of code may change the visibility of a View, only the View itself really knows what its current visibility is. The logic for ensuring that a View is visible can be error prone, and is often tied to the View itself.

في المقابل، تسهّل أداة Compose عرض دوال مركّبة مختلفة تمامًا باستخدام المنطق الشرطي في Kotlin:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

By design, CautionIcon doesn’t need to know or care why it is being displayed, and there is no concept of visibility: it either is in the Composition, or it isn’t.

من خلال الفصل الواضح بين إدارة الحالة ومنطق العرض، يمكنك تغيير طريقة عرض المحتوى بحرية أكبر كتحويل للحالة إلى واجهة مستخدم. تتيح لك إمكانية رفع الحالة عند الحاجة أيضًا إعادة استخدام الدوال المركّبة بشكل أكبر، لأنّ ملكية الحالة تكون أكثر مرونة.

الترويج للمكوّنات المغلَّفة والقابلة لإعادة الاستخدام

غالبًا ما يكون لدى عناصر View فكرة عن مكان وجودها: داخل Activity أو Dialog أو Fragment أو في مكان ما داخل تسلسل هرمي آخر لـ View. وبما أنّها غالبًا ما يتم تضخيمها من ملفات التنسيق الثابتة، فإنّ البنية العامة لـ View تميل إلى أن تكون جامدة جدًا. ويؤدي ذلك إلى زيادة الترابط، ويجعل من الصعب تغيير View أو إعادة استخدامه.

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

وهذه المشكلة أقل حدوثًا في Compose مع الدوال المركّبة القابلة لإعادة الاستخدام. يمكن للعناصر الرئيسية تحديد الحالة وعمليات الاستدعاء بسهولة، ما يتيح لك كتابة دوال مركّبة قابلة لإعادة الاستخدام بدون الحاجة إلى معرفة المكان المحدّد الذي سيتم استخدامها فيه.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

في المثال أعلاه، تكون الأجزاء الثلاثة أكثر تغليفًا وأقل اقترانًا:

  • يحتاج ImageWithEnabledOverlay فقط إلى معرفة الحالة الحالية لـ isEnabled، ولا يحتاج إلى معرفة أنّ ControlPanelWithToggle موجود أو حتى كيفية التحكّم فيه.

  • لا يعرف "ControlPanelWithToggle" أنّ "ImageWithEnabledOverlay" موجود. قد يكون هناك صفر أو طريقة واحدة أو أكثر لعرض isEnabled، ولن يتغيّر ControlPanelWithToggle.

  • بالنسبة إلى العنصر الرئيسي، لا يهم مدى تداخل ImageWithEnabledOverlay أو ControlPanelWithToggle. قد يقوم هؤلاء الأطفال بإجراء تغييرات أو تبديل المحتوى أو تمرير المحتوى إلى أطفال آخرين.

يُعرف هذا النمط باسم قلب التحكم، ويمكنك قراءة المزيد عنه في مستندات CompositionLocal.

التعامل مع التغييرات في حجم الشاشة

يُعدّ توفير موارد مختلفة لأحجام النوافذ المختلفة إحدى الطرق الرئيسية لإنشاء تنسيقات View متجاوبة. على الرغم من أنّ الموارد المؤهَّلة لا تزال خيارًا متاحًا لاتخاذ قرارات بشأن التنسيق على مستوى الشاشة، يسهّل Compose تغيير التنسيقات بالكامل في الرمز البرمجي باستخدام المنطق الشرطي العادي. لمزيد من المعلومات، يُرجى الاطّلاع على استخدام فئات حجم النافذة.

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

التمرير المُدمَج مع طرق العرض

لمزيد من المعلومات حول كيفية تفعيل إمكانية التشغيل التفاعلي للتمرير المُدمَج بين عناصر View القابلة للتمرير والدوال المركّبة القابلة للتمرير، والمُدمَجة في كلا الاتجاهين، يُرجى الاطّلاع على إمكانية التشغيل التفاعلي للتمرير المُدمَج.

إنشاء رسالة في "RecyclerView"

تتميّز الدوال المركّبة في RecyclerView بأداء عالٍ منذ الإصدار 1.3.0‎-alpha02 من RecyclerView. تأكَّد من استخدام الإصدار 1.3.0-alpha02 على الأقل من RecyclerView للاستفادة من هذه المزايا.

إمكانية التشغيل التفاعلي لـ "WindowInsets" مع Views

قد تحتاج إلى إلغاء الإعدادات التلقائية للهوامش عندما تحتوي شاشتك على كل من رمز &quot;طرق العرض&quot; ورمز &quot;Compose&quot; في التسلسل الهرمي نفسه. في هذه الحالة، عليك تحديد أيّ منهما يجب أن يستهلك المساحات المتروكة وأيّ منهما يجب أن يتجاهلها.

على سبيل المثال، إذا كان التنسيق الخارجي هو تنسيق Android View، يجب استخدام المساحات المتروكة في نظام العرض وتجاهلها في Compose. بدلاً من ذلك، إذا كان التنسيق الخارجي دالة مركّبة، يجب استخدام المساحات المتروكة في Compose، وإضافة مساحة متروكة إلى الدوال المركّبة AndroidView وفقًا لذلك.

By default, each ComposeView consumes all insets at the WindowInsetsCompat level of consumption. لتغيير هذا الإعداد التلقائي، اضبط ComposeView.consumeWindowInsets على false.

لمزيد من المعلومات، يُرجى الاطّلاع على مستندات WindowInsets في Compose.