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 को पैरामीटर के तौर पर एलान करने की ज़रूरत नहीं होती.

अहम शब्द: इस गाइड में, हम कंपोज़िशन, यूज़र इंटरफ़ेस (यूआई) ट्री, और यूज़र इंटरफ़ेस (यूआई) हैरारकी शब्दों का इस्तेमाल करते हैं. हालांकि, अन्य गाइड में इन्हें एक-दूसरे की जगह इस्तेमाल किया जा सकता है, लेकिन इनके अलग-अलग मतलब होते हैं:

  • कंपोज़िशन, ऐप्लिकेशन बनाने की सुविधा के कॉल ग्राफ़ का रिकॉर्ड होता है.
  • यूज़र इंटरफ़ेस (यूआई) ट्री या यूज़र इंटरफ़ेस (यूआई) की हैरारकी, LayoutNode का ट्री है. इसे कंपोज़िशन प्रोसेस के ज़रिए बनाया, अपडेट, और मैनेज किया जाता है.

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

तय करें कि आपको CompositionLocal का इस्तेमाल करना है या नहीं

कुछ ऐसी स्थितियां हैं जिनमें CompositionLocal आपके इस्तेमाल के लिए एक अच्छा समाधान हो सकता है:

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

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

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

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

CompositionLocal बनाएं

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

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

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

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 कॉन्टेंट लैम्ब्डा का इस्तेमाल करके भी ये फ़ायदे पाए जा सकते हैं:

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

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