ComposeLocal के साथ स्थानीय तौर पर स्कोप वाला डेटा

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 प्रॉपर्टी हैं, जिन्हें 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 इंस्टेंस का दायरा, कॉम्पोज़िशन के किसी हिस्से तक सीमित होता है, ताकि आप ट्री के अलग-अलग लेवल पर अलग-अलग वैल्यू दे सकें. CompositionLocal की current वैल्यू, कॉम्पोज़िशन के उस हिस्से में मौजूद किसी पैरंट की सबसे करीबी वैल्यू से मेल खाती है.

CompositionLocal को नई वैल्यू देने के लिए, CompositionLocalProvider और उसके provides इनफ़िक्स फ़ंक्शन का इस्तेमाल करें. यह फ़ंक्शन, CompositionLocal कुंजी को value से जोड़ता है. CompositionLocal की current प्रॉपर्टी को ऐक्सेस करने पर, CompositionLocalProvider के content Lambda फ़ंक्शन को दी गई वैल्यू मिलेगी. जब कोई नई वैल्यू दी जाती है, तो 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 कॉम्पोज़ेबल की झलक.

पिछले उदाहरण में, Material composables ने CompositionLocal इंस्टेंस का इंटरनल तौर पर इस्तेमाल किया था. CompositionLocal की मौजूदा वैल्यू ऐक्सेस करने के लिए, इसकी current प्रॉपर्टी का इस्तेमाल करें. यहां दिए गए उदाहरण में, LocalContext CompositionLocal की मौजूदा Context वैल्यू का इस्तेमाल, टेक्स्ट को फ़ॉर्मैट करने के लिए किया जाता है. आम तौर पर, 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 के उलट, staticCompositionLocalOf के रीड 'लिखें' सुविधा की मदद से ट्रैक नहीं किए जाते. वैल्यू बदलने से, कंपोज़िशन में सिर्फ़ उन जगहों के बजाय जहां CompositionLocal को फिर से कंपोज़ किया गया है, content लैम्डा की पूरी वैल्यू शामिल होती है.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")
    }
}

यह तरीका इस्तेमाल के कुछ मामलों में बेहतर हो सकता है, क्योंकि यह बच्चे को उसके पूर्वजों से अलग करता है. निचले लेवल के ज़्यादा फ़्लेक्सिबल कॉम्पोज़ेबल बनाने के लिए, अंस्टरसेटर कॉम्पोज़ेबल ज़्यादा जटिल हो जाते हैं.

इसी तरह, @Composable कॉन्टेंट lambdas को एक जैसे फ़ायदे पाने के लिए, इनका इस्तेमाल इसी तरह किया जा सकता है:

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

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