مع أنّ عملية نقل البيانات من Views إلى Compose مرتبطة بواجهة المستخدم فقط، هناك العديد من الأمور التي يجب أخذها في الاعتبار لإجراء عملية نقل بيانات آمنة وتدريجية. تحتوي هذه الصفحة على بعض الاعتبارات التي يجب مراعاتها أثناء نقل تطبيقك المستند إلى View إلى Compose.
نقل مظهر تطبيقك
Material Design هو نظام التصميم الذي يُنصح به لتطبيق السمات على تطبيقات Android.
بالنسبة إلى التطبيقات المستندة إلى العرض، تتوفّر ثلاثة إصدارات من Material:
- Material Design 1 باستخدام مكتبة AppCompat (أي
Theme.AppCompat.*) - Material Design 2 باستخدام مكتبة MDC-Android (أي
Theme.MaterialComponents.*) - Material Design 3 باستخدام مكتبة MDC-Android (أي
Theme.Material3.*)
بالنسبة إلى تطبيقات Compose، يتوفّر إصداران من Material:
- Material Design 2 باستخدام مكتبة Compose Material
(أي
androidx.compose.material.MaterialTheme) - Material Design 3 باستخدام مكتبة
Compose Material 3
(أي
androidx.compose.material3.MaterialTheme)
ننصحك باستخدام أحدث إصدار (Material 3) إذا كان نظام تصميم تطبيقك يتيح ذلك. تتوفّر أدلة نقل البيانات لكل من Views وCompose:
- المادة 1 إلى المادة 2 في المشاهدات
- الانتقال من Material 2 إلى Material 3 في "طرق العرض"
- المادة 2 إلى المادة 3 في "إنشاء"
عند إنشاء شاشات جديدة في Compose، بغض النظر عن إصدار Material Design الذي تستخدمه، احرص على تطبيق 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 ورمز 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() } }
يمكنك الاطّلاع على اختبار تخطيط "الكتابة الذكية" لمعرفة المزيد عن الاختبار. للحصول على معلومات حول إمكانية التشغيل التفاعلي مع أُطر اختبار واجهة المستخدم، راجِع إمكانية التشغيل التفاعلي مع 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 ورمز نظام 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 بالأداء العالي منذ الإصدار RecyclerView1.3.0-alpha02. تأكَّد من استخدام الإصدار 1.3.0-alpha02 على الأقل من
RecyclerView للاستفادة من هذه المزايا.
WindowInsets إمكانية التشغيل التفاعلي مع "طرق العرض"
قد تحتاج إلى تجاهل الإعدادات التلقائية للحواف الداخلية عندما تحتوي شاشتك على كلٍّ من طرق العرض ورمز Compose في التسلسل الهرمي نفسه. في هذه الحالة، عليك تحديد أيّهما يجب أن يستخدم الحواف الداخلية وأيّهما يجب أن يتجاهلها.
على سبيل المثال، إذا كان التصميم الخارجي هو تصميم Android View، عليك استخدام الحواف في نظام View وتجاهلها في Compose.
بدلاً من ذلك، إذا كان التنسيق الخارجي عبارة عن عنصر قابل للإنشاء، عليك استخدام الحواف الداخلية في Compose، وتعبئة العناصر القابلة للإنشاء AndroidView وفقًا لذلك.
بشكل تلقائي، يستهلك كل ComposeView جميع الحوافز عند WindowInsetsCompat مستوى الاستهلاك. لتغيير هذا السلوك التلقائي، اضبط قيمة
ComposeView.consumeWindowInsets
على false.
لمزيد من المعلومات، اطّلِع على مستندات WindowInsets في "الكتابة الذكية".
اقتراحات مخصصة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- عرض رموز الإيموجي
- Material Design 2 في Compose
- فواصل النوافذ في Compose