البيانات ذات النطاق المحلي باستخدام 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 كمَعلمة في الدالة المركّبة.

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

  • التركيب هو سجلّ الرسم البياني لاستدعاء الدوال المركّبة.
  • شجرة واجهة المستخدم أو التسلسل الهرمي لواجهة المستخدم هي شجرة LayoutNode التي يتم إنشاؤها وتعديلها وصيانتها من خلال عملية الإنشاء.

‫CompositionLocal هو ما يستخدمه المظهر المتعدد الأبعاد في الخلفية. 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 ودالة infix provides التي تربط مفتاح CompositionLocal بـ value. ستحصل دالة lambda‏ content في 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
الشكل 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 هي أداة لتمرير البيانات من خلال Composition بشكل ضمني.

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

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

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

علاوة على ذلك، قد لا يكون هناك مصدر واضح للحقيقة لهذه التبعية لأنها يمكن أن تتحول في أي جزء من التركيب. وبالتالي، قد يكون تصحيح أخطاء التطبيق عند حدوث مشكلة أكثر صعوبة لأنّك تحتاج إلى الانتقال إلى أعلى التركيب لمعرفة مكان تقديم قيمة current. توفّر أدوات مثل البحث عن الاستخدامات في بيئة التطوير المتكاملة (IDE) أو أداة فحص تنسيق 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 في Composition فقط.

إذا كان من غير المرجّح أن تتغيّر القيمة المقدَّمة إلى 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، استخدِم دالة infix 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()
    }
}