CompositionLocal هي أداة لتمرير البيانات من خلال التركيب بشكل ضمني. في هذه الصفحة، ستتعرّف على CompositionLocal بمزيد من التفصيل، وكيفية إنشاء CompositionLocal خاص بك، وما إذا كان CompositionLocal حلاً جيدًا لحالة الاستخدام لديك.
مقدّمة عن CompositionLocal
عادةً في Compose، تتدفق البيانات لأسفل من خلال شجرة واجهة المستخدم كمَعلمات لكل دالة مركّبة. ويجعل هذا التبعيات للدالة المركّبة واضحة. ومع ذلك، قد يكون هذا مرهقًا للبيانات التي يتم استخدامها بشكل متكرر وعلى نطاق واسع، مثل الألوان أو أنماط الكتابة. انظر المثال التالي:
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
لتجنُّب الحاجة إلى تمرير الألوان كمعلَمة صريحة تعتمد عليها معظم الدوال المركّبة، توفّر أداة Compose السمة CompositionLocal التي تتيح لك إنشاء عناصر مُسمّاة على مستوى الشجرة يمكن استخدامها كطريقة ضمنية لتدفق البيانات من خلال شجرة واجهة المستخدم.
يتم عادةً توفير عناصر CompositionLocal بقيمة في عقدة معيّنة
من شجرة واجهة المستخدم. ويمكن استخدام هذه القيمة من خلال العناصر الفرعية المركّبة بدون
تعريف CompositionLocal كمَعلمة في الدالة المركّبة.
المصطلحات الرئيسية: نستخدم في هذا الدليل مصطلحات الإنشاء وشجرة واجهة المستخدم والتسلسل الهرمي لواجهة المستخدم. على الرغم من أنّه قد يتم استخدامها بالتبادل في أدلة أخرى، إلا أنّها تحمل معاني مختلفة:
- التركيب هو سجلّ الرسم البياني لاستدعاء الدوال المركّبة.
- شجرة واجهة المستخدم أو التسلسل الهرمي لواجهة المستخدم هي شجرة
LayoutNodeالتي يتم إنشاؤها وتعديلها وصيانتها من خلال عملية الإنشاء.
CompositionLocal هو ما يستخدمه المظهر المتعدد الأبعاد في الخلفية.
MaterialTheme هو
عنصر يوفّر ثلاث نُسخ من CompositionLocal: colorScheme وtypography وshapes، ما يتيح لك استردادها لاحقًا في أي جزء فرعي
من التركيب.
على وجه التحديد، هذه هي السمات LocalColorScheme وLocalShapes وLocalTypography التي يمكنك الوصول إليها من خلال السمات MaterialTheme وcolorScheme وshapes وtypography.
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
يتم تحديد نطاق مثيل CompositionLocal لجزء من التركيب حتى تتمكن من تقديم قيم مختلفة على مستويات مختلفة من الشجرة. تتوافق قيمة current
لـ CompositionLocal مع أقرب قيمة يوفّرها
عنصر رئيسي في ذلك الجزء من التركيب.
لتقديم قيمة جديدة لـ CompositionLocal، استخدِم
CompositionLocalProvider
ودالة infix provides
التي تربط مفتاح CompositionLocal بـ value. ستحصل دالة lambda content في CompositionLocalProvider على القيمة المقدَّمة عند الوصول إلى السمة current في CompositionLocal. عند تقديم قيمة جديدة، يعيد Compose إنشاء أجزاء من Composition التي تقرأ CompositionLocal.
على سبيل المثال، يحتوي LocalContentColor CompositionLocal على لون المحتوى المفضّل المستخدَم للنص والرموز لضمان تباينه مع لون الخلفية الحالي. في المثال التالي، يتم استخدام CompositionLocalProvider لتقديم قيم مختلفة لأجزاء مختلفة من التركيب.
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. Surface { Column { Text("Uses Surface's provided content color") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { Text("Primary color provided by LocalContentColor") Text("This Text also uses primary as textColor") CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { DescendantExample() } } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text("This Text uses the error color now") }
CompositionLocalExampleفي المثال الأخير، تم استخدام مثيلات CompositionLocal داخليًا
من خلال دوال Material المركّبة. للوصول إلى القيمة الحالية لـ CompositionLocal،
استخدِم السمة current. في المثال التالي، يتم استخدام قيمة Context الحالية للسمة LocalContext CompositionLocal التي تُستخدم عادةً في تطبيقات Android لتنسيق النص:
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
صار بإمكانك إنشاء CompositionLocal الخاص بك
CompositionLocal هي أداة لتمرير البيانات من خلال Composition
بشكل ضمني.
من الإشارات الرئيسية الأخرى لاستخدام CompositionLocal عندما تكون المَعلمة
متقاطعة ولا يجب أن تكون الطبقات الوسيطة للتنفيذ على عِلم
بوجودها، لأنّ إعلام هذه الطبقات الوسيطة سيحدّ من
فائدة الدالة المركّبة. على سبيل المثال، يتم توفير طلب البحث عن أذونات Android من خلال CompositionLocal بشكل غير مرئي. يمكن للدالة المركّبة لأداة اختيار الوسائط إضافة وظائف جديدة للوصول إلى المحتوى المحمي بالأذونات على الجهاز بدون تغيير واجهة برمجة التطبيقات الخاصة بها وبدون الحاجة إلى أن يكون المتصلون بأداة اختيار الوسائط على دراية بهذا السياق الإضافي المستخدَم من البيئة.
ومع ذلك، لا يكون CompositionLocal دائمًا الحل الأفضل. لا ننصح بالإفراط في استخدام CompositionLocal لأنّ ذلك يؤدي إلى بعض السلبيات:
CompositionLocal يجعل سلوك الدالة المركّبة أكثر صعوبة في التفكير فيه. بما أنّها تنشئ تبعيات ضمنية، على المتصلين بالدوال المركّبة التي تستخدمها التأكّد من استيفاء قيمة لكل CompositionLocal.
علاوة على ذلك، قد لا يكون هناك مصدر واضح للحقيقة لهذه التبعية لأنها
يمكن أن تتحول في أي جزء من التركيب. وبالتالي، قد يكون تصحيح أخطاء التطبيق عند حدوث مشكلة أكثر صعوبة لأنّك تحتاج إلى الانتقال إلى أعلى التركيب لمعرفة مكان تقديم قيمة current. توفّر أدوات مثل البحث عن الاستخدامات في بيئة التطوير المتكاملة (IDE) أو أداة فحص تنسيق Compose معلومات كافية للتخفيف من حدة هذه المشكلة.
تحديد ما إذا كنت تريد استخدام CompositionLocal
هناك شروط معيّنة يمكن أن تجعل CompositionLocal حلاً جيدًا
لحالة الاستخدام:
يجب أن يكون لـ CompositionLocal قيمة تلقائية جيدة. إذا لم تكن هناك قيمة تلقائية، عليك التأكّد من أنّه من الصعب جدًا على المطوّر الوصول إلى حالة لا يتم فيها تقديم قيمة للسمة CompositionLocal.
قد يؤدي عدم توفير قيمة تلقائية إلى حدوث مشاكل وإحباط عند إنشاء
الاختبارات أو معاينة دالة مركّبة تستخدم CompositionLocal، لأنّها ستتطلب دائمًا
توفيرها بشكل صريح.
تجنَّب استخدام CompositionLocal للمفاهيم التي لا تُعتبر محدودة النطاق على مستوى الشجرة أو
محدودة النطاق على مستوى التسلسل الهرمي الفرعي. يكون استخدام CompositionLocal منطقيًا عندما يمكن استخدامه من قِبل أي عنصر فرعي، وليس من قِبل عدد قليل منها.
إذا كانت حالة الاستخدام لديك لا تستوفي هذه المتطلبات، يُرجى الاطّلاع على قسم
البدائل التي يجب وضعها في الاعتبار قبل إنشاء
CompositionLocal.
من الممارسات غير الصحيحة إنشاء CompositionLocal يحتوي على
ViewModel لشاشة معيّنة لكي تتمكّن جميع الدوال المركّبة في تلك الشاشة من
الحصول على مرجع إلى ViewModel لتنفيذ بعض العمليات المنطقية. هذه ممارسة سيئة لأنّه ليس من الضروري أن تعرف جميع الدوال المركّبة ضمن شجرة واجهة مستخدم معيّنة عن ViewModel. من الممارسات الجيدة أن يتم تمرير المعلومات التي تحتاجها الدوال المركّبة فقط، وذلك باتّباع النمط الذي تتدفق فيه الحالة إلى الأسفل والأحداث إلى الأعلى.
سيجعل هذا النهج الدوال المركّبة أكثر قابلية لإعادة الاستخدام وأسهل في الاختبار.
إنشاء CompositionLocal
هناك واجهتا برمجة تطبيقات لإنشاء CompositionLocal:
compositionLocalOf: يؤدي تغيير القيمة المقدَّمة أثناء إعادة الإنشاء إلى إبطال فقط المحتوى الذي يقرأ قيمةcurrent.staticCompositionLocalOf: على عكسcompositionLocalOf، لا يتتبّع Compose عمليات قراءةstaticCompositionLocalOf. يؤدي تغيير القيمة إلى إعادة إنشاء دالة lambdacontentبالكامل حيث يتم توفيرCompositionLocal، بدلاً من إعادة إنشاء الأماكن التي تتم فيها قراءة قيمةcurrentفي Composition فقط.
إذا كان من غير المرجّح أن تتغيّر القيمة المقدَّمة إلى CompositionLocal أو لن تتغيّر أبدًا، استخدِم staticCompositionLocalOf للاستفادة من مزايا الأداء.
على سبيل المثال، قد يكون نظام تصميم التطبيق صارمًا في طريقة إبراز الدوال المركّبة باستخدام ظل لمكوّن واجهة المستخدم. بما أنّ الارتفاعات المختلفة للتطبيق يجب أن تنتشر في شجرة واجهة المستخدم، نستخدم CompositionLocal. بما أنّ قيمة CompositionLocal يتم اشتقاقها بشكل مشروط
استنادًا إلى مظهر النظام، نستخدم واجهة برمجة التطبيقات compositionLocalOf:
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
تقديم قيم لـ CompositionLocal
تربط الدالة المركّبة CompositionLocalProvider القيم بمثيلات CompositionLocal للتسلسل الهرمي المحدّد. لتقديم قيمة جديدة إلى CompositionLocal، استخدِم دالة infix
provides
التي تربط مفتاح CompositionLocal بـ value على النحو التالي:
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
استخدام CompositionLocal
تعرض CompositionLocal.current القيمة التي يوفّرها أقرب
CompositionLocalProvider يوفّر قيمة لـ CompositionLocal:
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
بدائل يمكن استخدامها
قد يكون CompositionLocal حلاً مفرطًا لبعض حالات الاستخدام. إذا كانت حالة الاستخدام لديك لا تستوفي المعايير المحدّدة في قسم تحديد ما إذا كان يجب استخدام CompositionLocal، قد يكون هناك حلّ آخر أكثر ملاءمةً لحالة الاستخدام لديك.
تمرير مَعلمات صريحة
من الممارسات الجيدة تحديد تبعيات الدوال المركّبة بوضوح. ننصحك بتمرير الدوال المركّبة فقط عند الحاجة إليها. لتشجيع الفصل بين الدوال المركّبة وإعادة استخدامها، يجب أن تحتوي كل دالة مركّبة على أقل قدر ممكن من المعلومات.
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
Inversion of control
هناك طريقة أخرى لتجنُّب تمرير التبعيات غير الضرورية إلى دالة مركّبة، وهي استخدام عكس التحكم. بدلاً من أن يأخذ العنصر التابع تبعية لتنفيذ بعض المنطق، يقوم العنصر الرئيسي بذلك بدلاً من ذلك.
انظر المثال التالي حيث يحتاج العنصر التابع إلى تشغيل الطلب لتحميل بعض البيانات:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
استنادًا إلى الحالة، قد تقع على عاتق "MyDescendant" مسؤولية كبيرة. بالإضافة إلى ذلك،
يؤدي تمرير MyViewModel كعنصر تابع إلى جعل MyDescendant أقل قابلية لإعادة الاستخدام لأنّهما
أصبحا مرتبطين معًا. لنفترض أنّنا استخدمنا بديلاً لا يمرّر التبعية إلى العنصر التابع، بل يستخدم مبادئ "انعكاس التحكّم" التي تجعل العنصر الرئيسي مسؤولاً عن تنفيذ المنطق:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
قد يكون هذا النهج أكثر ملاءمةً لبعض حالات الاستخدام لأنّه يفصل العنصر الفرعي عن العناصر السابقة المباشرة. تميل الدوال المركّبة الأصلية إلى أن تصبح أكثر تعقيدًا لصالح الدوال المركّبة ذات المستوى الأدنى الأكثر مرونة.
وبالمثل، يمكن استخدام دوال lambda الخاصة بالمحتوى @Composable بالطريقة نفسها للحصول على المزايا نفسها:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- بنية المظهر في Compose
- استخدام طرق العرض في Compose
- Kotlin لـ Jetpack Compose