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

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 كمَعلمة في الدالة المركّبة.

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
الشكل 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.

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

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

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

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

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

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

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

إنشاء CompositionLocal

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

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

  • staticCompositionLocalOf: على عكس compositionLocalOf، لا يتم تتبُّع قراءات staticCompositionLocalOf من خلال Compose. سيؤدي تغيير القيمة إلى إعادة إنشاء دالة content lambda بالكامل التي يتم فيها توفير 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، استخدِم الدالة 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 الخاصة بمحتوى @Composable بالطريقة نفسها للحصول على المزايا نفسها:

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

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