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

مع أنّ عملية الانتقال من Views إلى Compose مرتبطة بشكل أساسي بواجهة المستخدم، هناك العديد من الأمور التي يجب أخذها في الاعتبار لإجراء عملية انتقال آمنة وتدريجية. تحتوي هذه الصفحة على بعض الاعتبارات التي يجب مراعاتها أثناء نقل تطبيقك المستند إلى View إلى 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)
  • ‫Material Design 3 باستخدام مكتبة Compose Material 3 (أي androidx.compose.material3.MaterialTheme)

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

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

تستخدم جميع أمثلة Jetpack Compose تصميمًا مخصّصًا لـ Compose يستند إلى MaterialTheme.

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

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

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

بعد نقل أجزاء من تطبيقك إلى 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 في "الكتابة"

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

عند استخدام Compose، احرص على عدم استخدام النوع ViewModel نفسه في عناصر قابلة للإنشاء مختلفة لأنّ عناصر ViewModel تتّبع نطاقات دورة حياة View. سيكون النطاق إما نشاط المضيف أو الجزء أو الرسم البياني للتنقّل في حال استخدام مكتبة 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 ورمز نظام View إلى مشاركة البيانات. ننصحك، عند الإمكان، بتضمين الحالة المشترَكة في فئة أخرى تتّبع أفضل الممارسات المتعلّقة بالدوال المعرَّفة من قِبل المستخدم (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.

النظر إلى النظام باعتباره مصدر المعلومات الموثوق

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

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

إنشاء رسالة إلكترونية في RecyclerView

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

WindowInsets التوافق مع "طرق العرض"

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

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

بشكل تلقائي، يستهلك كل ComposeView جميع الحواف الداخلية عند مستوى الاستهلاك WindowInsetsCompat. لتغيير هذا السلوك التلقائي، اضبط ComposeView.consumeWindowInsets على false.

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