البيانات ذات النطاق المحلي باستخدام 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
    )
}

لدعم عدم الحاجة إلى تمرير الألوان كتبعية صريحة للمَعلمة إلى معظم العناصر القابلة للإنشاء، توفّر ميزة "إنشاء" 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. عند توفير قيمة جديدة، تعيد ميزة 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")
}

الشكل 1: معاينة العنصر القابل للتجميع CompositionLocalExample

في المثال الأخير، تم استخدام مثيلات CompositionLocal داخليًا بواسطة المواد القابلة للإنشاء. للوصول إلى القيمة الحالية لسمة 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، لا تتتبّع أداة Compose عمليات قراءة staticCompositionLocalOf. يؤدي تغيير القيمة إلى إعادة تركيب content لامدا بالكامل حيث يتم تقديم CompositionLocal، بدلاً من الأماكن التي تتم فيها قراءة قيمة current في التركيب فقط.

إذا كان من غير المرجّح أن تتغيّر القيمة المقدَّمة لسمة CompositionLocal أو لن تتغيّر أبدًا، استخدِم staticCompositionLocalOf للاستفادة من مزايا الأداء.

على سبيل المثال، قد يتم النظر في نظام تصميم التطبيق بطريقة رفع العناصر القابلة للإنشاء باستخدام ظل لمكون واجهة المستخدم. وبما أنّ الارتفاعات المختلفة للتطبيق يجب أن يتم نشرها في شجرة واجهة المستخدم، نستخدم CompositionLocal. بما أنّ قيمة CompositionLocal يتم اشتقاقها بشروط استنادًا إلى مظهر النظام، نستخدم compositionLocalOf API:

// 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")
    }
}

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

يمكن أيضًا استخدام دالة lambdas في محتوى @Composable بالطريقة نفسها للحصول على المزايا نفسها:

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

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