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

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

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

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

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

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

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

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

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

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

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

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

إذا كنت تستخدم مكوّن Navigation في تطبيقك، اطّلِع على التنقّل باستخدام Compose - إمكانية التشغيل التفاعلي و نقل بيانات Jetpack Navigation إلى Navigation Compose لمزيد من المعلومات.

اختبار واجهة المستخدم المختلطة في Compose/طرق العرض

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

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

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 مع بنية تطبيقك الحالية

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

استخدام ViewModel في Compose

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

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

على سبيل المثال، إذا كانت العناصر المركّبة مستضافة في نشاط، تعرض الدالة 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. في هذه الحالة، يتم الاحتفاظ بمصدر الحقيقة في عنصر مركّب يرسل تعديلات الحالة.

على سبيل المثال، قد تتيح لك مكتبة "إحصاءات Google" تقسيم قاعدة بيانات المستخدمين من خلال إرفاق بيانات وصفية مخصّصة (خصائص المستخدم في هذا المثال) بجميع أحداث "إحصاءات Google" اللاحقة. لإعلام مكتبة "إحصاءات Google" بنوع المستخدم الحالي، استخدِم 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 والشاشات المستندة إلى طرق العرض.

في 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 قابلة للنفخ والاستخدام، مثل طريقة العرض التقليدية. في ما يلي مثال على ذلك باستخدام View Binding أدناه:

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. على الرغم من أنّ أجزاء أخرى من الرمز قد تغيّر مدى ظهور View، فإنّ View نفسها فقط تعرف مدى ظهورها الحالي. قد يكون منطق ضمان ظهور View عرضةً للأخطاء، وغالبًا ما يكون مرتبطًا بـ View نفسها.

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

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

حسب التصميم، لا يحتاج CautionIcon إلى معرفة سبب ظهوره أو الاهتمام به، ولا يوجد مفهوم visibility: إما أن يكون في التركيب أو لا يكون.

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

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

غالبًا ما يكون لدى عناصر 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 القابلة للتمرير والعناصر المركّبة القابلة للتمرير، والمتداخلة في كلا الاتجاهَين، اطّلِع على إمكانية التشغيل التفاعلي للتمرير المتداخل.

استخدام Compose في RecyclerView

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

إمكانية التشغيل التفاعلي لـ WindowInsets مع طرق العرض

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

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

تلقائيًا، يستهلك كلّ ComposeView جميع عمليات الإزاحة على مستوى WindowInsetsCompat من الاستهلاك. لتغيير هذا السلوك التلقائي، اضبط ComposeView.consumeWindowInsets على false.

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