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

किसी CompositionLocal को नई वैल्यू देने के लिए, CompositionLocalProvider का इस्तेमाल करें. साथ ही, इसके provides इनफ़िक्स फ़ंक्शन का इस्तेमाल करें. यह फ़ंक्शन, CompositionLocal की कुंजी को value से जोड़ता है. content lambda of the CompositionLocalProvider will get the provided value when accessing the current property of the CompositionLocal. नई वैल्यू दिए जाने पर, 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 कंपोज़ेबल की झलक.
पहली इमेज. CompositionLocalExample कंपोज़ेबल का प्रीव्यू.

पिछले उदाहरण में, CompositionLocal के इंस्टेंस का इस्तेमाल, Material कंपोज़ेबल ने अंदरूनी तौर पर किया था. किसी CompositionLocal की मौजूदा वैल्यू को ऐक्सेस करने के लिए, उसकी current प्रॉपर्टी का इस्तेमाल करें. यहां दिए गए उदाहरण में, टेक्स्ट को फ़ॉर्मैट करने के लिए, Context वैल्यू का इस्तेमाल किया गया है. इसका इस्तेमाल आम तौर पर, Android ऐप्लिकेशन में किया जाता है:LocalContextCompositionLocal

@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 वैल्यू कहां दी गई थी. आईडीई में इस्तेमाल ढूंढें या Compose लेआउट इंस्पेक्टर जैसे टूल, इस समस्या को कम करने के लिए ज़रूरी जानकारी उपलब्ध कराते हैं.

CompositionLocal का इस्तेमाल करना है या नहीं, यह तय करना

कुछ ऐसी शर्तें हैं जिनकी वजह से, CompositionLocal आपके इस्तेमाल के मामले के लिए एक अच्छा विकल्प बन सकता है:

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

ऐसे कॉन्सेप्ट के लिए CompositionLocal का इस्तेमाल करने से बचें जिन्हें ट्री-स्कोप या सब-हायरार्की स्कोप नहीं माना जाता. CompositionLocal तब काम का होता है, जब इसका इस्तेमाल कुछ डिसेंडेंट के बजाय, सभी डिसेंडेंट कर सकते हों.

अगर आपका इस्तेमाल का मामला इन ज़रूरी शर्तों को पूरा नहीं करता है, तो विकल्पों पर विचार करें सेक्शन देखें CompositionLocal.

गलत तरीके का एक उदाहरण, किसी खास स्क्रीन का ViewModel रखने वाला CompositionLocal बनाना है, ताकि उस स्क्रीन में मौजूद सभी कंपोज़ेबल, कुछ लॉजिक को पूरा करने के लिए ViewModel का रेफ़रंस पा सकें. यह एक गलत तरीका है, क्योंकि किसी खास यूज़र इंटरफ़ेस (यूआई) ट्री के नीचे मौजूद सभी कंपोज़ेबल को ViewModel के बारे में जानने की ज़रूरत नहीं होती. सही तरीका यह है कि कंपोज़ेबल को सिर्फ़ वह जानकारी पास की जाए जिसकी उन्हें ज़रूरत है. इसके लिए, स्टेट नीचे की ओर और इवेंट ऊपर की ओर पास करने के पैटर्न का इस्तेमाल किया जाए. इस तरीके से, आपके कंपोज़ेबल को बार-बार इस्तेमाल किया जा सकेगा और उनकी जांच करना आसान हो जाएगा.

CompositionLocal बनाना

CompositionLocal बनाने के लिए, दो एपीआई उपलब्ध हैं:

  • compositionLocalOf: कंपोज़िशन के दौरान दी गई वैल्यू में बदलाव करने पर, सिर्फ़ वह कॉन्टेंट अमान्य होता है जो इसकी current वैल्यू को पढ़ता है.

  • staticCompositionLocalOf: compositionLocalOf के उलट, Compose, staticCompositionLocalOf के रीड को ट्रैक नहीं करता. वैल्यू में बदलाव करने पर, content lambda का पूरा हिस्सा फिर से कंपोज़ होता है. इसमें 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 को वैल्यू देना

The 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 की दी गई वैल्यू को दिखाता है. यह 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 कॉन्टेंट lambda का इस्तेमाल भी इसी तरह किया जा सकता है, ताकि वही फ़ायदे मिल सकें:

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

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