البيانات ذات النطاق المحلي باستخدام MembershipLocal

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 التي يمكنك الوصول إليها من خلال السمات 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 ودالة التوسيط provides التي تربط مفتاح CompositionLocal بمفتاح value. ستحصل دالة lambda content في CompositionLocalProvider على القيمة المقدَّمة عند الوصول إلى سمة current في CompositionLocal. عند تقديم قيمة جديدة، يعيد تطبيق "الإنشاء" تركيب أجزاء من التركيب التي تقرأ 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 هي أداة لنقل البيانات للأسفل من خلال التركيب بشكل ضمني.

إشارة رئيسية أخرى لاستخدام CompositionLocal هي عندما تكون المَعلمة شاملة ويجب ألا تكون الطبقات الوسيطة للتنفيذ على دراية بوجودها، لأنّ جعل هذه الطبقات الوسيطة على دراية بها سيحدّ من فائدة العنصر القابل للتجميع. على سبيل المثال، يتم تنفيذ طلبات البحث عن أذونات Android من خلال CompositionLocal في الخلفية. يمكن أن تضيف أداة اختيار الوسائط القابلة للتركيب وظائف جديدة للوصول إلى المحتوى المحمي بإذن على الجهاز بدون تغيير واجهة برمجة التطبيقات الخاصة بها ومطالبة مُستخدِمي أداة اختيار الوسائط بمعرفة هذا السياق المُضاف المستخدَم من البيئة.

ومع ذلك، لا يُعدّ CompositionLocal الحلّ الأفضل دائمًا. ننصح بعدم إساءة استخدام CompositionLocal لأنّ ذلك قد يؤدي إلى بعض النتائج السلبية:

CompositionLocal يجعل من الصعب تفسير سلوك العنصر القابل للتجميع. وبما أنّها تُنشئ تبعيات ضمنية، على مُستخدِمي العناصر القابلة للتجميع الذين يستخدمونها التأكّد من توفّر قيمة لكلّ CompositionLocal.

بالإضافة إلى ذلك، قد لا يتوفّر مصدر واضح لهذه التبعية لأنّه يمكن أن تتغيّر في أي جزء من التركيب. وبالتالي، قد يكون تصحيح أخطاء التطبيق عند حدوث مشكلة أكثر صعوبة لأنّك تحتاج إلى الانتقال إلى أعلى التركيبة لمعرفة مكان تقديم قيمة current. توفّر أدوات مثل العثور على موارد الاستعمال في IDE أو أداة فحص تنسيق الإنشاء معلومات كافية للتخفيف من هذه المشكلة.

تحديد ما إذا كنت تريد استخدام CompositionLocal

هناك شروط معيّنة يمكن أن تجعل CompositionLocal حلًا جيدًا لحالة الاستخدام:

يجب أن تحتوي سمة CompositionLocal على قيمة تلقائية جيدة. إذا لم تكن هناك قيمة CompositionLocalتلقائية، يجب التأكّد من أنّه من الصعب جدًا على المطوّر أن يقع في موقف لا يتم فيه تقديم قيمة لسمة CompositionLocal. يمكن أن يؤدي عدم تقديم قيمة تلقائية إلى حدوث مشاكل وإزعاج عند إنشاء اختبارات أو معاينة عنصر قابل للتجميع يستخدم CompositionLocal، لأنّه سيتطلب دائمًا تقديمها صراحةً.

تجنَّب استخدام CompositionLocal للعناصر التي لا تُعتبر على مستوى الشجرة أو على مستوى التسلسل الهرمي الفرعي. يكون استخدام CompositionLocal منطقيًا عندما يمكن استخدامه من قِبل أي نسل، وليس من قِبل بضعة أنسال.

إذا لم تستوفِ حالة الاستخدام هذه المتطلبات، اطّلِع على القسم البدائل التي يجب أخذها في الاعتبار قبل إنشاء CompositionLocal.

من الأمثلة على الممارسات غير الصحيحة إنشاء CompositionLocal يحتوي على ViewModel شاشة معيّنة لكي تتمكّن كل العناصر القابلة للتجميع في تلك الشاشة من الحصول على مرجع إلى ViewModel لتنفيذ بعض المنطق. هذه ممارسة سيئة لأنّ بعض العناصر القابلة للتجميع ضمن شجرة واجهة مستخدِم معيّنة لا تحتاج إلى معرفة ViewModel. من أفضل الممارسات عدم تمرير سوى المعلومات التي تحتاجها العناصر القابلة للتجميع، وذلك باتّباع النمط تتدفق الحالة للأسفل والأحداث للأعلى. سيؤدي هذا النهج إلى جعل العناصر القابلة للتجميع أكثر قابلية لإعادة الاستخدام وأسهل في الاختبار.

إنشاء CompositionLocal

تتوفّر واجهتَا برمجة تطبيقات لإنشاء CompositionLocal:

  • compositionLocalOf: يؤدي تغيير القيمة المقدَّمة أثناء إعادة التركيب إلى إلغاء صلاحية المحتوى الذي يقرأ قيمة current فقط.

  • staticCompositionLocalOf: على عكس compositionLocalOf، لا يتم تتبُّع عمليات قراءة staticCompositionLocalOf من خلال Compose. يؤدي تغيير القيمة إلى إعادة تركيب 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")
    }
}

يمكن أن يكون هذا النهج مناسبًا بشكل أفضل لبعض حالات الاستخدام، لأنّه يفصل بين العنصر الفرعي وعناصره الأصلية المباشرة. تميل العناصر المكوّنة للعناصر الرئيسية إلى أن تصبح أكثر تعقيدًا، وذلك لتوفير عناصر مركبة أكثر مرونة في المستويات الدنيا.

وبالمثل، يمكن استخدام @Composable لامدا للمحتوى بالطريقة نفسها للحصول على الفوائد نفسها:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}