Jetpack Compose के लिए Kotlin

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

डिफ़ॉल्ट आर्ग्युमेंट

Kotlin फ़ंक्शन लिखते समय, फ़ंक्शन आर्ग्युमेंट के लिए डिफ़ॉल्ट वैल्यू तय की जा सकती हैं, इनका इस्तेमाल तब किया जाता है, जब कॉलर साफ़ तौर पर उन वैल्यू को पास नहीं करता. इस सुविधा से, ओवरलोड किए गए फ़ंक्शन की ज़रूरत कम हो जाती है.

उदाहरण के लिए, मान लें कि आपको एक ऐसा फ़ंक्शन लिखना है जो स्क्वेयर बनाता है. उस फ़ंक्शन में एक ज़रूरी पैरामीटर हो सकता है, sideLength. इससे हर साइड की लंबाई तय की जाती है . इसमें कई ज़रूरी नहीं पैरामीटर हो सकते हैं, जैसे कि thickness, edgeColor वगैरह. अगर कॉलर इन्हें तय नहीं करता है, तो फ़ंक्शन डिफ़ॉल्ट वैल्यू का इस्तेमाल करता है. अन्य लैंग्वेज में, आपको कई फ़ंक्शन लिखने पड़ सकते हैं:

// We don't need to do this in Kotlin!
void drawSquare(int sideLength) { }

void drawSquare(int sideLength, int thickness) { }

void drawSquare(int sideLength, int thickness, Color edgeColor) { }

Kotlin में, एक फ़ंक्शन लिखा जा सकता है और आर्ग्युमेंट के लिए डिफ़ॉल्ट वैल्यू तय की जा सकती हैं:

fun drawSquare(
    sideLength: Int,
    thickness: Int = 2,
    edgeColor: Color = Color.Black
) {
}

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

drawSquare(30, 5, Color.Red);

इसके उलट, यह कोड सेल्फ़-डॉक्यूमेंटिंग है:

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

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

Text(text = "Hello, Android!")

उस कोड का असर, ज़्यादा जानकारी वाले इस कोड जैसा ही होता है. इसमें के ज़्यादा Text पैरामीटर साफ़ तौर पर सेट किए गए हैं:

Text(
    text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

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

हाई-ऑर्डर फ़ंक्शन और लैम्डा एक्सप्रेशन

Kotlin, _हाई-ऑर्डर फ़ंक्शन_ के साथ काम करता है. ये ऐसे फ़ंक्शन होते हैं जो अन्य फ़ंक्शन को पैरामीटर के तौर पर लेते हैं. Compose, इसी तरीके पर आधारित है. उदाहरण के लिए, कंपोज़ेबल फ़ंक्शन, Button लैम्डा पैरामीटर उपलब्ध कराता है.onClick उस पैरामीटर की वैल्यू एक फ़ंक्शन होती है. जब उपयोगकर्ता बटन पर क्लिक करता है, तो बटन उस फ़ंक्शन को कॉल करता है:

Button(
    // ...
    onClick = myClickFunction
)
// ...

हाई-ऑर्डर फ़ंक्शन, लैम्डा एक्सप्रेशन के साथ स्वाभाविक तौर पर काम करते हैं. ये ऐसे एक्सप्रेशन होते हैं जो किसी फ़ंक्शन की वैल्यू देते हैं. अगर आपको फ़ंक्शन की ज़रूरत सिर्फ़ एक बार है, तो उसे हाई-ऑर्डर फ़ंक्शन में पास करने के लिए, कहीं और तय करने की ज़रूरत नहीं है. इसके बजाय, लैम्डा एक्सप्रेशन की मदद से, फ़ंक्शन को वहीं तय किया जा सकता है. पिछले उदाहरण में, यह माना गया है कि myClickFunction() को कहीं और तय किया गया है. हालांकि, अगर आपको उस फ़ंक्शन का इस्तेमाल सिर्फ़ यहां करना है, तो लैम्डा एक्सप्रेशन की मदद से, फ़ंक्शन को इनलाइन तय करना ज़्यादा आसान है:

Button(
    // ...
    onClick = {
        // do something
        // do something else
    }
) { /* ... */ }

ट्रेलिंग लैम्डा

Kotlin, हाई-ऑर्डर फ़ंक्शन को कॉल करने के लिए एक खास सिंटैक्स उपलब्ध कराता है. इसका आखिरी पैरामीटर एक लैम्डा होता है. अगर आपको उस पैरामीटर के तौर पर लैम्डा एक्सप्रेशन पास करना है, तो ट्रेलिंग लैम्डा सिंटैक्स का इस्तेमाल किया जा सकता है. लैम्डा एक्सप्रेशन को ब्रैकेट में रखने के बजाय, उसे बाद में रखा जाता है. Compose में यह एक सामान्य स्थिति है. इसलिए, आपको यह पता होना चाहिए कि कोड कैसा दिखता है.

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

Column(
    modifier = Modifier.padding(16.dp),
    content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

चूंकि content पैरामीटर, फ़ंक्शन सिग्नेचर में आखिरी पैरामीटर है और हम इसकी वैल्यू को लैम्डा एक्सप्रेशन के तौर पर पास कर रहे हैं, इसलिए इसे ब्रैकेट से बाहर निकाला जा सकता है:

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

दोनों उदाहरणों का मतलब बिलकुल एक ही है. ब्रेसेस, content पैरामीटर में पास किए गए लैम्डा एक्सप्रेशन को तय करते हैं.

असल में, अगर पास किया जाने वाला सिर्फ़ पैरामीटर, ट्रेलिंग लैम्डा है—यानी, अगर आखिरी पैरामीटर एक लैम्डा है और कोई अन्य पैरामीटर पास नहीं किया जा रहा है—तो ब्रैकेट को पूरी तरह से छोड़ा जा सकता है. इसलिए, उदाहरण के लिए, मान लें कि आपको Column में कोई मॉडिफ़ायर पास करने की ज़रूरत नहीं है. आपके पास इस तरह कोड लिखने का विकल्प होता है:

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

Compose में यह सिंटैक्स काफ़ी सामान्य है. खास तौर पर, Column जैसे लेआउट एलिमेंट के लिए. आखिरी पैरामीटर एक लैम्डा एक्सप्रेशन होता है, जो एलिमेंट के चाइल्ड तय करता है. इन चाइल्ड को फ़ंक्शन कॉल के बाद, ब्रेसेस में तय किया जाता है.

स्कोप और रिसीवर

कुछ तरीके और प्रॉपर्टी सिर्फ़ किसी खास स्कोप में उपलब्ध होती हैं. सीमित स्कोप की मदद से, ऐसी सुविधा उपलब्ध कराई जा सकती है जिसकी ज़रूरत है. साथ ही, गलती से उस सुविधा का इस्तेमाल करने से बचा जा सकता है जिसकी ज़रूरत नहीं है.

Compose में इस्तेमाल किया गया कोई उदाहरण देखें. जब Row लेआउट कंपोज़ेबल को कॉल किया जाता है, तो आपका कॉन्टेंट लैम्डा, RowScope में अपने-आप लागू हो जाता है. इससे Row को ऐसी सुविधा उपलब्ध कराने में मदद मिलती है जो सिर्फ़ Row में मान्य है. नीचे दिए गए उदाहरण से पता चलता है कि Row ने align मॉडिफ़ायर के लिए, पंक्ति के हिसाब से कोई वैल्यू कैसे उपलब्ध कराई है:

Row {
    Text(
        text = "Hello world",
        // This Text is inside a RowScope so it has access to
        // Alignment.CenterVertically but not to
        // Alignment.CenterHorizontally, which would be available
        // in a ColumnScope.
        modifier = Modifier.align(Alignment.CenterVertically)
    )
}

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

Box(
    modifier = Modifier.drawBehind {
        // This method accepts a lambda of type DrawScope.() -> Unit
        // therefore in this lambda we can access properties and functions
        // available from DrawScope, such as the `drawRectangle` function.
        drawRect(
            /*...*/
            /* ...
        )
    }
)

ज़्यादा जानकारी के लिए, Kotlin के दस्तावेज़ में, रिसीवर के साथ फ़ंक्शन लिटरल देखें.

दी गई प्रॉपर्टी

Kotlin, दी गई प्रॉपर्टी के साथ काम करता है. इन प्रॉपर्टी को फ़ील्ड की तरह कॉल किया जाता है, लेकिन इनकी वैल्यू, एक्सप्रेशन की जांच करके डाइनैमिक तरीके से तय की जाती है. by सिंटैक्स के इस्तेमाल से, इन प्रॉपर्टी की पहचान की जा सकती है:

class DelegatingClass {
    var name: String by nameGetterFunction()

    // ...
}

अन्य कोड, इस तरह के कोड से प्रॉपर्टी को ऐक्सेस कर सकते हैं:

val myDC = DelegatingClass()
println("The name property is: " + myDC.name)

println() के लागू होने पर, स्ट्रिंग की वैल्यू पाने के लिए, nameGetterFunction() को कॉल किया जाता है.

ये दी गई प्रॉपर्टी, खास तौर पर तब काम आती हैं, जब स्टेट-बैक वाली प्रॉपर्टी के साथ काम किया जा रहा हो:

var showDialog by remember { mutableStateOf(false) }

// Updating the var automatically triggers a state change
showDialog = true

डेटा क्लास को डीस्ट्रक्चर करना

अगर कोई डेटा क्लास तय की जाती है, तो डीस्ट्रक्चरिंग एलान की मदद से, डेटा को आसानी से ऐक्सेस किया जा सकता है. उदाहरण के लिए, मान लें कि आपने Person क्लास तय की है:

data class Person(val name: String, val age: Int)

अगर आपके पास उस टाइप का कोई ऑब्जेक्ट है, तो इस तरह के कोड से उसकी वैल्यू को ऐक्सेस किया जा सकता है:

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Compose फ़ंक्शन में अक्सर इस तरह का कोड दिखता है:

Row {

    val (image, title, subtitle) = createRefs()

    // The `createRefs` function returns a data object;
    // the first three components are extracted into the
    // image, title, and subtitle variables.

    // ...
}

डेटा क्लास, कई अन्य काम की सुविधाएं उपलब्ध कराती हैं. उदाहरण के लिए, जब कोई डेटा क्लास तय की जाती है, तो कंपाइलर अपने-आप equals() और copy() जैसे काम के फ़ंक्शन तय करता है. ज़्यादा जानकारी के लिए, डेटा क्लास का दस्तावेज़ देखें.

सिंगलटन ऑब्जेक्ट

Kotlin में सिंगलटन का एलान करना आसान है. ये ऐसी क्लास होती हैं जिनका हमेशा एक और सिर्फ़ एक इंस्टेंस होता है. इन सिंगलटन का एलान, object कीवर्ड से किया जाता है. Compose में अक्सर ऐसे ऑब्जेक्ट का इस्तेमाल किया जाता है. उदाहरण के लिए, MaterialTheme को सिंगलटन ऑब्जेक्ट के तौर पर तय किया जाता है. MaterialTheme.colors, shapes, और typography प्रॉपर्टी में, मौजूदा थीम की वैल्यू शामिल होती हैं.

टाइप-सेफ़ बिल्डर और DSL

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

Jetpack Compose, कुछ एपीआई के लिए डीएसएल का इस्तेमाल करता है. जैसे, LazyRow और LazyColumn.

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        // Add a single item as a header
        item {
            Text("Message List")
        }

        // Add list of messages
        items(messages) { message ->
            Message(message)
        }
    }
}

Kotlin, रिसीवर के साथ फ़ंक्शन लिटरल का इस्तेमाल करके, टाइप-सेफ़ बिल्डर की गारंटी देता है. अगर हम Canvas कंपोज़ेबल को उदाहरण के तौर पर लें, तो यह पैरामीटर के तौर पर, DrawScope को रिसीवर के तौर पर इस्तेमाल करने वाला फ़ंक्शन लेता है. जैसे, onDraw: DrawScope.() -> Unit. इससे कोड का ब्लॉक, DrawScope में तय किए गए मेंबर फ़ंक्शन को कॉल कर पाता है.

Canvas(Modifier.size(120.dp)) {
    // Draw grey background, drawRect function is provided by the receiver
    drawRect(color = Color.Gray)

    // Inset content by 10 pixels on the left/right sides
    // and 12 by the top/bottom
    inset(10.0f, 12.0f) {
        val quadrantSize = size / 2.0f

        // Draw a rectangle within the inset bounds
        drawRect(
            size = quadrantSize,
            color = Color.Red
        )

        rotate(45.0f) {
            drawRect(size = quadrantSize, color = Color.Blue)
        }
    }
}

Kotlin के दस्तावेज़ में, टाइप-सेफ़ बिल्डर और डीएसएल के बारे में ज़्यादा जानें .

Kotlin कोरूटीन

कोरूटीन, Kotlin में लैंग्वेज लेवल पर एसिंक्रोनस प्रोग्रामिंग की सुविधा उपलब्ध कराते हैं. कोरूटीन, थ्रेड को ब्लॉक किए बिना, एक्ज़ीक्यूशन को सस्पेंड कर सकते हैं. रिस्पॉन्सिव यूज़र इंटरफ़ेस (यूआई) स्वाभाविक तौर पर एसिंक्रोनस होता है. Jetpack Compose, कॉलबैक का इस्तेमाल करने के बजाय, एपीआई लेवल पर कोरूटीन का इस्तेमाल करके इस समस्या को हल करता है.

Jetpack Compose, ऐसे एपीआई उपलब्ध कराता है जिनकी मदद से, यूज़र इंटरफ़ेस (यूआई) लेयर में कोरूटीन का सुरक्षित तरीके से इस्तेमाल किया जा सकता है. The rememberCoroutineScope फ़ंक्शन, CoroutineScope दिखाता है. इसकी मदद से, इवेंट हैंडलर में कोरूटीन बनाए जा सकते हैं और Compose सस्पेंड एपीआई को कॉल किया जा सकता है. ScrollState के animateScrollTo एपीआई का इस्तेमाल करने वाला यह उदाहरण देखें.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button(
    // ...
    onClick = {
        // Create a new coroutine that scrolls to the top of the list
        // and call the ViewModel to load data
        composableScope.launch {
            scrollState.animateScrollTo(0) // This is a suspend function
            viewModel.loadData()
        }
    }
) { /* ... */ }

डिफ़ॉल्ट रूप से, कोरूटीन, कोड के ब्लॉक को क्रम से लागू करते हैं. सस्पेंड फ़ंक्शन को कॉल करने वाला कोई कोरूटीन, सस्पेंड फ़ंक्शन के रिटर्न होने तक, अपने एक्ज़ीक्यूशन को सस्पेंड करता है. यह तब भी लागू होता है, जब सस्पेंड फ़ंक्शन, एक्ज़ीक्यूशन को किसी दूसरे CoroutineDispatcher पर ले जाता है. पिछले उदाहरण में, loadData तब तक लागू नहीं होगा, जब तक सस्पेंड फ़ंक्शन animateScrollTo रिटर्न नहीं होता.

कोड को एक साथ लागू करने के लिए, नए कोरूटीन बनाने की ज़रूरत होती है. ऊपर दिए गए उदाहरण में, स्क्रीन के सबसे ऊपर स्क्रोल करने और viewModel से डेटा लोड करने के लिए, दो कोरूटीन की ज़रूरत होती है.

// Create a CoroutineScope that follows this composable's lifecycle
val composableScope = rememberCoroutineScope()
Button( // ...
    onClick = {
        // Scroll to the top and load data in parallel by creating a new
        // coroutine per independent work to do
        composableScope.launch {
            scrollState.animateScrollTo(0)
        }
        composableScope.launch {
            viewModel.loadData()
        }
    }
) { /* ... */ }

कोरूटीन की मदद से, एसिंक्रोनस एपीआई को जोड़ना आसान हो जाता है. यहां दिए गए उदाहरण में, स्क्रीन पर टैप करने पर किसी एलिमेंट की पोज़िशन को ऐनिमेट करने के लिए, pointerInput मॉडिफ़ायर को ऐनिमेशन एपीआई के साथ जोड़ा गया है.

@Composable
fun MoveBoxWhereTapped() {
    // Creates an `Animatable` to animate Offset and `remember` it.
    val animatedOffset = remember {
        Animatable(Offset(0f, 0f), Offset.VectorConverter)
    }

    Box(
        // The pointerInput modifier takes a suspend block of code
        Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                // Create a new CoroutineScope to be able to create new
                // coroutines inside a suspend function
                coroutineScope {
                    while (true) {
                        // Wait for the user to tap on the screen and animate
                        // in the same block
                        awaitPointerEventScope {
                            val offset = awaitFirstDown().position

                            // Launch a new coroutine to asynchronously animate to
                            // where the user tapped on the screen
                            launch {
                                // Animate to the pressed position
                                animatedOffset.animateTo(offset)
                            }
                        }
                    }
                }
            }
    ) {
        Text("Tap anywhere", Modifier.align(Alignment.Center))
        Box(
            Modifier
                .offset {
                    // Use the animated offset as the offset of this Box
                    IntOffset(
                        animatedOffset.value.x.roundToInt(),
                        animatedOffset.value.y.roundToInt()
                    )
                }
                .size(40.dp)
                .background(Color(0xff3c1361), CircleShape)
        )
    }

कोरूटीन के बारे में ज़्यादा जानने के लिए, Android पर Kotlin कोरूटीन गाइड देखें.