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

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