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