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 كمَعلمة في الدالة المركّبة.
المصطلحات الأساسية: في هذا الدليل، نستخدم المصطلحات التركيب وشجرة واجهة المستخدم والتسلسل الهرمي لواجهة المستخدم. على الرغم من أنّه قد يتم استخدام المصطلحين بالتبادل في أدلة أخرى، إلا أنّ لهما معاني مختلفة:
- التركيب هو سجلّ الرسم البياني للمكالمات الخاص بالدوال القابلة للإنشاء.
- شجرة واجهة المستخدم أو التسلسل الهرمي لواجهة المستخدم هي شجرة
LayoutNodeيتم إنشاؤها وتعديلها وصيانتها من خلال عملية الإنشاء.
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") }
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. سيؤدي تغيير القيمة إلى إعادة إنشاء دالة lambda بالكامل التي يتوفّر فيهاcontent، بدلاً من إعادة إنشاء الأماكن التي تتم فيها قراءة قيمةcurrentفي Composition فقط.CompositionLocal
إذا كانت القيمة المقدَّمة إلى 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 }
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