البيانات ذات النطاق المحلي باستخدام 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 التي يمكنك الوصول إليها من خلال سمات 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.
الشكل 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. توفر أدوات مثل العث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()
    }
}