CompositionLocal
هي أداة لتمرير البيانات بشكل ضِمني من خلال Composition. في هذه الصفحة، ستتعرّف بالتفصيل على 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
كمَعلمة في الدالة القابلة للإنشاء.
CompositionLocal
هو ما يستخدمه مظهر Material في الخلفية.
MaterialTheme
هو عنصر يوفّر ثلاث مثيلات CompositionLocal
: colorScheme
وtypography
وshapes
، ما يتيح لك استردادها لاحقًا في أي جزء فرعي من Composition.
على وجه التحديد، هذه هي الخصائص 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
بجزء من Composition حتى تتمكّن من تقديم قيم مختلفة على مستويات مختلفة من الشجرة. تتوافق قيمة current
الخاصة بـ CompositionLocal
مع أقرب قيمة يقدّمها عنصر رئيسي في هذا الجزء من التركيب.
لتوفير قيمة جديدة إلى CompositionLocal
، استخدِم
CompositionLocalProvider
ودالة provides
الوسطية التي تربط مفتاح CompositionLocal
بقيمة value
. ستحصل دالة
content
lambda الخاصة بـ 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") }
الشكل 1. معاينة العنصر القابل للإنشاء 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
.
بالإضافة إلى ذلك، قد لا يكون هناك مصدر واضح للحقيقة لهذه التبعية لأنّه يمكن أن تتغير في أي جزء من التركيب. وبالتالي، قد يكون تصحيح أخطاء التطبيق عند حدوث مشكلة أكثر صعوبة لأنّ عليك الانتقال إلى أعلى Composition لمعرفة مكان توفير القيمة current
. توفّر أدوات مثل Find
usages في بيئة التطوير المتكاملة أو أداة فحص التنسيق في Compose معلومات كافية
لحلّ هذه المشكلة.
تحديد ما إذا كنت ستستخدم CompositionLocal
هناك شروط معيّنة يمكن أن تجعل CompositionLocal
حلاً جيدًا لحالة الاستخدام الخاصة بك:
يجب أن تتضمّن CompositionLocal
قيمة تلقائية جيدة. إذا لم تتوفّر قيمة تلقائية، عليك ضمان أنّه من الصعب جدًا أن يواجه المطوّر حالة لا يتم فيها تقديم قيمة للسمة CompositionLocal
.
قد يؤدي عدم توفير قيمة تلقائية إلى حدوث مشاكل وإحباط عند إنشاء اختبارات أو معاينة عنصر قابل للإنشاء يستخدم CompositionLocal
، وسيتطلّب ذلك دائمًا توفير قيمة بشكل صريح.
تجنَّب استخدام CompositionLocal
للمفاهيم التي لا يُنظر إليها على أنّها ضمن نطاق شجرة أو ضمن نطاق التسلسل الهرمي الفرعي. يكون استخدام CompositionLocal
منطقيًا عندما يمكن لأي عنصر فرعي الاستفادة منه، وليس لعدد قليل منها.
إذا كانت حالة الاستخدام لا تستوفي هذه المتطلبات، يُرجى الاطّلاع على قسم البدائل التي يجب أخذها في الاعتبار قبل إنشاء CompositionLocal
.
من الأمثلة على الممارسات غير الصحيحة إنشاء CompositionLocal
يحتوي على ViewModel
لشاشة معيّنة، ما يتيح لجميع العناصر القابلة للإنشاء في تلك الشاشة الحصول على مرجع إلى ViewModel
لتنفيذ بعض العمليات المنطقية. وهذه ممارسة سيئة
لأنّه ليس من الضروري أن تعرف جميع العناصر القابلة للإنشاء التي تقع أسفل شجرة واجهة مستخدم معيّنة قيمة ViewModel
. تتمثّل الممارسة الجيدة في تمرير المعلومات التي تحتاج إليها العناصر القابلة للإنشاء فقط، وذلك باتّباع النمط انتقال الحالة إلى الأسفل وانتقال الأحداث إلى الأعلى. ستجعل هذه الطريقة عناصرك القابلة للإنشاء أكثر قابلية لإعادة الاستخدام وأسهل في الاختبار.
إنشاء CompositionLocal
تتوفّر واجهتا برمجة تطبيقات لإنشاء CompositionLocal
:
compositionLocalOf
: يؤدي تغيير القيمة المقدَّمة أثناء إعادة التركيب إلى إبطال المحتوى الذي يقرأ قيمةcurrent
فقط.staticCompositionLocalOf
: على عكسcompositionLocalOf
، لا يتتبّع Compose عدد مرات قراءةstaticCompositionLocalOf
. سيؤدي تغيير القيمة إلى إعادة إنشاء دالةcontent
lambda بالكامل التي يتم فيها توفير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
، استخدِم الدالة 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 }
انعكاس التحكّم
هناك طريقة أخرى لتجنُّب تمرير التبعيات غير الضرورية إلى عنصر قابل للإنشاء وهي من خلال عكس التحكّم. بدلاً من أن يتلقّى العنصر التابع عنصرًا تابعًا لتنفيذ بعض المنطق، ينفّذ العنصر الرئيسي ذلك بدلاً منه.
في ما يلي مثال على حالة يحتاج فيها عنصر فرعي إلى بدء طلب لتحميل بعض البيانات:
@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