على الرغم من أنّ نقل البيانات من "طرق العرض" إلى "الإنشاء" مرتبط بواجهة المستخدم فقط، هناك الكثير من الأمور التي يجب أخذها في الاعتبار لإجراء عملية نقل بيانات آمنة ومتزايدة. تحتوي هذه الصفحة على بعض النقاط التي يجب مراعاتها أثناء نقل تطبيقك المستنِد إلى "العرض" إلى "الإنشاء".
نقل مظهر تطبيقك
يُعدّ Material Design نظام التصميم المُقترَح لاستخدامه في تصميم تطبيقات Android.
بالنسبة إلى التطبيقات المستندة إلى View، تتوفّر ثلاثة إصدارات من 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) إذا كان نظام تصميم تطبيقك يسمح بذلك. تتوفّر أدلة نقل البيانات لكلٍّ من "العروض" و"الإنشاء":
- المادّة 1 إلى المادة 2 في "المشاهدات"
- المادّة 2 إلى المادة 3 في "المشاهدات"
- المادّة 2 إلى المادة 3 في ميزة "الإنشاء"
عند إنشاء شاشات جديدة في Compose، بغض النظر عن إصدار Material
Design الذي تستخدمه، تأكَّد من تطبيق MaterialTheme
قبل أي
عناصر قابلة للتجميع تُنشئ واجهة مستخدم من مكتبات Compose Material. تعتمد مكونات Material (Button
وText
وما إلى ذلك) على توفّر MaterialTheme
ولا يمكن تحديد سلوكها بدونه.
تستخدِم كل عيّنات Jetpack Compose
مظهرًا مخصّصًا لتطبيق Compose تم إنشاؤه استنادًا إلى MaterialTheme
.
اطّلِع على أنظمة التصميم في Compose ونقل مظاهر XML إلى Compose لمزيد من المعلومات.
التنقّل
إذا كنت تستخدم مكوّن Navigation في تطبيقك، يمكنك الاطّلاع على التنقّل باستخدام Compose - إمكانية التشغيل التفاعلي ونقل بيانات Jetpack Navigation إلى Navigation Compose للحصول على مزيد من المعلومات.
اختبار واجهة مستخدِم مختلطة لميزة "إنشاء"/"المشاهدات"
بعد نقل أجزاء من تطبيقك إلى Compose، من المهم اختبارها للتأكّد من عدم حدوث أي مشاكل.
عندما يستخدم نشاط أو جزء ميزة "الإنشاء"، عليك استخدام
createAndroidComposeRule
بدلاً من استخدام ActivityScenarioRule
. يدمج createAndroidComposeRule
ActivityScenarioRule
مع ComposeTestRule
يتيح لك اختبار ميزة "إنشاء" و
عرض الرمز في الوقت نفسه.
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
في ميزة "الكتابة الذكية"
إذا كنت تستخدم مكتبة مكونات البنية
ViewModel
، يمكنك الوصول إلى
ViewModel
من أيّ عنصر قابل للتركيب من خلال
استدعاء دالة
viewModel()
، كما هو موضّح في مقالة Compose والمكتبات الأخرى.
عند استخدام Compose، يجب الانتباه إلى استخدام نوع ViewModel
نفسه في
عناصر Compose المختلفة لأنّ عناصر 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") ?: "") } } }
مصدر بيانات الحالة
عند استخدام ميزة "الإنشاء" في جزء من واجهة المستخدم، من المحتمل أن تحتاج ميزة "الإنشاء" ورمز نظام "العرض" إلى مشاركة البيانات. ننصحك، كلما أمكن،
بتغليف هذه الحالة المشتركة في فئة أخرى تلتزم بأفضل ممارسات الدوالّ المخصّصة
التي تستخدمها كلتا المنصّتَين، على سبيل المثال، في ViewModel
التي تعرض بثًا لل data
المشترَكة لإصدار تعديلات البيانات.
ومع ذلك، قد لا يكون ذلك ممكنًا دائمًا إذا كانت البيانات التي ستتم مشاركتها قابلة للتغيير أو مرتبطة بشدّة بعنصر واجهة مستخدم. في هذه الحالة، يجب أن يكون أحد النظامَين مصدر الحقيقة، ويجب أن يشارك هذا النظام أي تعديلات على البيانات مع النظام الآخر. كقاعدة عامة، يجب أن يكون مصدر المعلومات مملوكًا للعنصر الأقرب إلى جذر التدرّج الهرمي لواجهة المستخدم.
إنشاء المحتوى كمصدر للحقائق
استخدِم العنصر
SideEffect
composable لنشر حالة 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، ننصحك
بتغليف الحالة في كائنات mutableStateOf
لجعلها آمنة لسلسلة المهام في
Compose. في حال استخدام هذا النهج، يتم تبسيط الدوالّ القابلة للتجميع لأنّه
لم يعُد لديها مصدر المعلومات، ولكنّ نظام View يحتاج إلى تعديل الحالة
المتغيّرة وViews التي تستخدم هذه الحالة.
في المثال التالي، يحتوي العنصر 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 } }
نقل واجهة المستخدم المشتركة
إذا كنت بصدد نقل البيانات تدريجيًا إلى ميزة "الإنشاء"، قد تحتاج إلى استخدام عناصر واجهة مستخدم مشتركة في كلّ من "الإنشاء" ونظام "العرض". على سبيل المثال، إذا كان تطبيقك يتضمّن CallToActionButton
مخصّصًا، قد تحتاج إلى استخدامه في كلّ من الشاشات المستندة إلى CallToActionButton
وCallToActionButton
.
في أداة "الإنشاء"، تصبح عناصر واجهة المستخدم المشترَكة عناصر قابلة للتجميع يمكن إعادة استخدامها في
التطبيق، بغض النظر عمّا إذا كان العنصر مصمّمًا باستخدام 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
. على الرغم من أنّه
قد تغيّر أجزاء أخرى من الرمز البرمجي مستوى ظهور 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 لإنشاء واجهات مستخدم قابلة للتكيّف.
الانتقال المتداخل باستخدام "العروض"
لمزيد من المعلومات حول كيفية تفعيل إمكانية التشغيل التفاعلي للانتقال المتداخل بين عناصر العرض القابلة للانتقال والمكوّنات القابلة للانتقال، والتي تكون متداخلة في كلا الاتجاهين، اطّلِع على مقالة إمكانية التشغيل التفاعلي للانتقال المتداخل.
إنشاء الرسائل في RecyclerView
تحقّق العناصر القابلة للتجميع في RecyclerView
أداءً جيدًا منذ الإصدار RecyclerView
1.3.0-alpha02. تأكَّد من استخدام الإصدار 1.3.0-alpha02 على الأقل من
RecyclerView
للاستفادة من هذه المزايا.
WindowInsets
إمكانية التشغيل التفاعلي مع "الملف الشخصي على Google"
قد تحتاج إلى إلغاء الأجزاء المضمّنة التلقائية عندما تحتوي شاشتك على كلّ من "طرق العرض" و "رمز الإنشاء" في التسلسل الهرمي نفسه. في هذه الحالة، عليك تحديد بوضوح العنصر الذي يجب أن يستخدِم المكوّنات المضمّنة والعنصر الذي يجب أن يتجاهلها.
على سبيل المثال، إذا كان التنسيق الخارجي هو تنسيق Android View، يجب
استخدام الأجزاء المضمّنة في نظام View وتجاهلها في Compose.
بدلاً من ذلك، إذا كان التنسيق الخارجي هو عنصر قابل للتركيب، يجب استخدام
العناصر المضمّنة في أداة "الإنشاء"، وإضافة مساحة بين العناصر القابلة للتركيب AndroidView
وفقًا لذلك.
يستهلك كل ComposeView
تلقائيًا جميع المكوّنات المضمّنة على مستوى الاستهلاك
WindowInsetsCompat
. لتغيير هذا السلوك التلقائي، اضبط
ComposeView.consumeWindowInsets
على false
.
لمزيد من المعلومات، يُرجى الاطّلاع على مستندات WindowInsets
في Compose.
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون لغة JavaScript غير مفعّلة.
- عرض رموز الإيموجي
- تصميم Material Design 2 في ميزة "الإنشاء"
- أجزاء النافذة المضمّنة في ميزة "الإنشاء"