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 से जोड़ता है. 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 प्रॉपर्टी का इस्तेमाल करें. इस उदाहरण में, Android ऐप्लिकेशन में आम तौर पर इस्तेमाल होने वाले LocalContext CompositionLocal की मौजूदा Context वैल्यू का इस्तेमाल, टेक्स्ट को फ़ॉर्मैट करने के लिए किया गया है:

@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 बनाने से पहले, विचार करने लायक विकल्प सेक्शन देखें.

गलत तरीके से कोड लिखने का एक उदाहरण यह है कि किसी स्क्रीन के CompositionLocal को होल्ड करने वाला ViewModel बनाया जाए, ताकि उस स्क्रीन के सभी कंपोज़ेबल, कुछ लॉजिक को पूरा करने के लिए 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 से मिली वैल्यू दिखाता है. यह 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()
    }
}