ऐप्लिकेशन फ़ाउंडेशन

Compose में टेक्स्ट एडिटर की भरोसेमंद सुविधाएं बनाना

स्पीकर

  • अनस्तेसिया सोबोलेवा

    सॉफ़्टवेयर इंजीनियर

  • हाओयू ज़ैंग

    सीनियर सॉफ़्टवेयर इंजीनियर

इसके बारे में

बातचीत के बारे में जानकारी

Compose में भरोसेमंद टेक्स्ट एडिटिंग की सुविधा बनाने के लिए, BasicTextField को पूरी तरह से फिर से लिखा गया. बफ़र-ड्रिवन आर्किटेक्चर पर स्विच करने से, आईएमई की रेस कंडिशन की समस्या हमेशा के लिए खत्म हो गई. इस सेशन में बताया गया है कि लेगसी इनपुट सिंक करने की सुविधा को क्यों हटाना पड़ा. साथ ही, यह भी बताया गया है कि TextFieldState, कीबोर्ड के अपडेट को कैसे सिंक रखता है. इसके बाद, फ़ॉर्मैट किए गए टेक्स्ट में बदलाव करने की सुविधा के बारे में ज़्यादा जानकारी पाएं. इसमें TrackedRange के काम करने के तरीके, बदलाव करने वाले ब्लॉक की परफ़ॉर्मेंस की लागत, और स्टाइल की सीमाओं के पीछे की असल समस्याओं के बारे में जानें. इसके बाद, Markdown रेंडरिंग की सुविधा के बारे में जानें.

खास जानकारी

  • TextFieldState एपीआई ओवरलोड, Compose में कॉलबैक पर आधारित ओवरलोड की जगह लेता है. यह उपयोगकर्ता के टाइप करने के साथ-साथ सिंक होकर अपडेट होता है. इससे सॉफ़्टवेयर कीबोर्ड के साथ समय से जुड़ी समस्याएं और ग़ैर-ज़रूरी रीकंपोज़िशन की समस्या नहीं होती.
  • इनपुट और आउटपुट ट्रांसफ़ॉर्मेशन की मदद से, TextFieldBuffer का इस्तेमाल करके टेक्स्ट में बदलाव किया जा सकता है. बदलावों को लागू करने से पहले, उन्हें रोका जा सकता है. इसके अलावा, ऑफ़सेट मैपिंग को मैन्युअल तरीके से कंप्यूट किए बिना, डिसप्ले टेक्स्ट को फ़ॉर्मैट किया जा सकता है.
  • TextFieldState को होस्ट करना, आपके आर्किटेक्चर की ज़रूरतों पर निर्भर करता है. इसे कंपोज़ेबल में स्थानीय तौर पर सेव किया जा सकता है या कारोबारी नियम के ऐक्सेस के लिए, सीधे ViewModel में सेव किया जा सकता है.
  • Compose 1.12 में, TextFieldBuffer के लिए फ़ॉर्मैट किए गए टेक्स्ट में बदलाव करने वाले एपीआई जोड़े गए हैं. ExpandPolicy के लिए उपलब्ध विकल्पों का इस्तेमाल करके, अपनी पसंद के मुताबिक स्टाइल लागू किए जा सकते हैं. साथ ही, उपयोगकर्ताओं के बदलाव करने पर, मौजूदा स्टाइल रेंज अपने-आप अपडेट हो जाती हैं.
  • Jetpack Compose के लिए, मार्कअप टेक्स्ट की एक नई लाइब्रेरी पर काम चल रहा है. यह Markdown जैसे फ़ॉर्मैट के साथ काम करता है. इसमें लाइव एडिटिंग और स्ट्रीमिंग के लिए, इंक्रीमेंटल पार्सिंग और रेंडरिंग की सुविधा मिलती है.

संसाधन

Gemini Notebook

विषय

  • jetpack-compose
  • textfieldstate
  • टेक्स्ट में बदलाव करने की सुविधा
  • basictextfield
  • formatted-text
  • input-transformation

ट्रांसक्रिप्ट

ट्रांसक्रिप्ट डाउनलोड करें

Compose में टेक्स्ट में बदलाव करने की सुविधा के बारे में जानकारी

  • नमस्ते!

  • हमारी बातचीत में शामिल होने के लिए धन्यवाद.

  • मेरा नाम ऐनस्टेशिया है और मेरे साथ मेरे कलीग हाओयू हैं.

  • आज हम आपको बताएंगे कि टेक्स्ट में बदलाव करने की सुविधा को भरोसेमंद कैसे बनाया जा सकता है.

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

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

    TextField(state = rememberTextFieldState())
    OutlinedTextField(state = rememberTextFieldState())
  • अगर आपको अपना डिज़ाइन सिस्टम बनाना है या आपको बदलाव करने के लिए एक आसान बॉक्स चाहिए, तो इसके लिए भी एक BasicTextField है.

    BasicTextField(state = rememberTextFieldState())
  • इसके अलावा, SecureTextField भी है. इसे खास तौर पर पासवर्ड डालने के लिए डिज़ाइन किया गया है.

    SecureTextField(state = rememberTextFieldState())
  • BasicTextField एक अनस्टाइल कॉम्पोनेंट है. यह Compose में मौजूद अन्य सभी टेक्स्ट फ़ील्ड को बेहतर बनाता है.

  • इसलिए, आपने पिछले कुछ सालों में देखा होगा कि हमारे पास दो एपीआई शेप हैं. इनमें से एक वैल्यू और वैल्यू में बदलाव का कॉलबैक लेता है और दूसरा TextFieldState ऑब्जेक्ट लेता है.

    @Composable
    public fun BasicTextField(
        state: TextFieldState,
    )
  • ऐसे में, आपको लग सकता है कि हम किस विकल्प का सुझाव देते हैं.

  • हमारा सुझाव है कि आप TextFieldState ओवरलोड का इस्तेमाल करें.

  • इस बातचीत में, हम आपको बताएंगे कि एपीआई, सुविधाओं, और परफ़ॉर्मेंस के मामले में इसके क्या फ़ायदे हैं.

  • Compose 1.7 के बाद से, यह स्टेबल है.

  • कॉल बैक ओवरलोड के लिए, हम Compose 1.13 में BasicTextField और मटीरियल रैपर में उन्हें धीरे-धीरे बंद कर रहे हैं.

    @Composable
    public fun BasicTextField(
        value: String,
        onValueChange: (String) -> Unit
    )
    
    @Composable
    public fun BasicTextField(
        value: TextFieldValue,
        onValueChange: (TextFieldValue) -> Unit
  • इसका मतलब है कि हम इसमें कोई नई सुविधा नहीं जोड़ेंगे या गड़बड़ियां ठीक नहीं करेंगे. हालांकि, सुरक्षा से जुड़ी गंभीर समस्याओं या सुलभता से जुड़ी समस्याओं को ठीक किया जाएगा.

  • वैल्यू चेंज ओवरलोड पर स्टेटलेस वैल्यू, सिंक्रोनस इनपुट के लिए बहुत अच्छी तरह से काम करती है.

  • हालांकि, जब इसे एसिंक्रोनस स्ट्रीम के साथ जोड़ा गया, जैसे कि StateFlow, तो इससे आईएमई या तकनीकी भाषा में कहें, तो सॉफ़्टवेयर कीबोर्ड के साथ टाइमिंग से जुड़ी समस्याएं आईं.

  • इस वजह से, वर्णों के छूटने, डुप्लीकेट वर्णों के दिखने, और कर्सर के जंप होने की समस्या आई. इस रिकॉर्डिंग में यह देखा जा सकता है.

पेश है TextFieldState

  • इस समस्या को पूरी तरह से ठीक करने के लिए, हमने TextFieldState को लॉन्च किया.

  • जब उपयोगकर्ता टाइप करता है, तो यह एसिंक्रोनस राउंड ट्रिप का इंतज़ार किए बिना, सिंक्रोनस तरीके से अपडेट होता है. आईएमई को इसी की ज़रूरत थी.

  • TextFieldState में, सिर्फ़ पढ़ने के लिए उपलब्ध टेक्स्ट के अलावा, अन्य प्रॉपर्टी भी होती हैं. इनसे, बदलाव करने का एक ऐक्टिव सेशन बनता है.

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

  • TextFieldState की इंटरनल प्रॉपर्टी, Compose Snapshot System की मदद से काम करती हैं.

  • इसका मतलब है कि जब Compose में किसी जगह पर उस TextFieldState ऑब्जेक्ट से टेक्स्ट या चुने गए हिस्से को पढ़ा जाता है, तो उस खास स्कोप को अपडेट की सदस्यता मिल जाती है जहां से उसे पढ़ा गया है.

  • इससे BasicTextField को लेआउट या ड्रॉ फ़ेज़ में सिर्फ़ स्टेट को पढ़ने की अनुमति मिलती है. साथ ही, कुछ अन्य चीज़ों के साथ मिलकर, यह उपयोगकर्ता के टाइप करने पर होने वाले ग़ैर-ज़रूरी रीकंपोज़िशन को खत्म करता है.

  • साथ ही, कुछ अन्य बदलावों के साथ, इसने TextFieldState ओवरलोड के लिए परफ़ॉर्मेंस में काफ़ी सुधार किया.

  • अब, अगर TextFieldState का टेक्स्ट रीड-ओनली प्रॉपर्टी है, तो इनपुट फ़ील्ड में प्रोग्राम के हिसाब से बदलाव कैसे किए जा सकते हैं?

  • ऐसे में, TextFieldBuffer काम आता है.

  • यह एक ऐसा ऑब्जेक्ट है जिसमें बदलाव किया जा सकता है. यह आपको बदलाव करने के लिए एपीआई उपलब्ध कराता है.

  • इसलिए, जब आपको प्रोग्राम के हिसाब से बदलाव करना हो, तो TextFieldState के एडिट फ़ंक्शन को कॉल करें.

  • इस लैम्ब्डा के अंदर, आपको TextFieldBuffer मिलता है.

  • TextFieldBuffer कई फ़ंक्शन दिखाता है. इनकी मदद से, ज़रूरत के हिसाब से वर्णों को शामिल किया जा सकता है, बदला जा सकता है, और मिटाया जा सकता है.

  • बदलाव करने के लिए एक सलाह यह है कि उन्हें एक के बाद एक कई बार कॉल न करें.

    // Often generated by AI agents:
    state.edit { insert(5, ",") }
    state.edit { delete(12, 13) }
    state.edit { replace(0, 5, "goodbye") }
    state.edit { selection = TextRange(9, 16) }
  • इसके बजाय, अपने सभी ऑपरेशन को एक ही एडिट कॉल में बैच करें.

    // Batch all operations in a single atomic edit:
    state.edit {
    insert(5, ",")
    delete(12, 13)
    replace(0, 5, "goodbye")
    selection = TextRange(9, 16)
    }
  • इस तरह, Compose, TextFieldBuffer का सिर्फ़ एक इंस्टेंस बनाएगा, उसमें बदलाव करेगा, और फिर उसे आईएमई और स्नैपशॉट सिस्टम के साथ एक बार सिंक करेगा.

  • एक और सलाह यह है कि आपको जिन सामान्य बैच ऑपरेशन की ज़रूरत होगी उनके लिए, हमारे पास TextFieldState पर एक्सटेंशन फ़ंक्शन मौजूद हैं.

    fun TextFieldState.setTextAndPlaceCursorAtEnd(text: String) {
    edit {
    replace(0, length, text)
    placeCursorAtEnd()
    }
    }
  • इसलिए, आपको उन कार्रवाइयों को बैच में करने के बारे में सोचने की ज़रूरत नहीं है.

  • बस इनमें से किसी एक नंबर पर कॉल करें.

  • उदाहरण के लिए, clearText और setTextAndPlaceCursorAtEnd.

    fun TextFieldState.clearText() {
    edit {
    delete(0, length)
    placeCursorAtEnd()
    }
    }

इनपुट को फ़िल्टर और फ़ॉर्मैट करना

  • अब उपयोगकर्ता के इनपुट को फ़िल्टर करने के बारे में क्या ख्याल है?

  • जैसे, वर्ण सीमा लागू करना या सिर्फ़ अंकों को स्वीकार करना.

  • पुराने एपीआई में, टेक्स्ट को पहले ही कमिट कर दिए जाने के बाद, onValueChange में फ़िल्टर करने की सुविधा काम करती थी.

    onValueChange { newString ->
      // full diffing new String vs old String
    }
  • इसलिए, उस इनपुट को अस्वीकार करने या बदलने से कीबोर्ड के साथ समस्या होती थी.

  • साथ ही, onValueChange कॉलबैक ने आपको सिर्फ़ फ़ाइनल स्ट्रिंग दी.

  • इसलिए, यह समझने के लिए कि क्या बदला है, आपको पूरी स्ट्रिंग की तुलना करनी पड़ती थी.

  • इसके बजाय, नए ओवरलोड में हमारे पास इनपुट ट्रांसफ़ॉर्मेशन है.

    @Stable
    public fun interface InputTransformation {
        public fun TextFieldBuffer.transformInput()
    }
  • इसलिए, यह उपयोगकर्ता के इनपुट को टेक्स्ट फ़ील्ड की स्थिति तक पहुंचने से पहले ही रोक देता है. साथ ही, आपको वही TextFieldBuffer एपीआई देता है.

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

    // Intercept and filter input during typing
    inputTransformation = InputTransformation {
        val text = asCharSequence()
        if (!text.all { it.isDigit() }) {
            // Reject non-digit characters
            revertAllChanges()
        }
    }
  • यह जांच की जाती है कि क्या इसमें अंकों के अलावा कोई और वर्ण है. अगर ऐसा है, तो उस TextFieldBuffer पर revertAllChanges को कॉल करके इनपुट को अस्वीकार कर दिया जाता है.

  • इनपुट ट्रांसफ़ॉर्मेशन की मुख्य सुविधा ChangeList है.

  • इसलिए, पूरी स्ट्रिंग को अलग-अलग करने के बजाय, यह आपको बदलाव की सटीक रेंज की जांच करने की सुविधा देता है. इसके बाद, ज़रूरत पड़ने पर उन्हें उसी जगह पर बदला जा सकता है या इनपुट को पूरी तरह से अस्वीकार किया जा सकता है.

  • Compose, कीबोर्ड और कर्सर को अपने-आप सिंक रखेगा.

  • आखिर में, अगर आपको अपने इनपुट की विज़ुअल फ़ॉर्मैटिंग करनी है, तो क्या करना होगा?

  • उदाहरण के लिए, आपको क्रेडिट कार्ड नंबर या फ़ोन नंबर में ब्रैकेट या डैश जोड़ने हैं.

  • सचमुच।

  • साथ ही, आपको उन्हें अपने डेटाबेस में सेव नहीं करना है, है न?

  • पहले, आपने विज़ुअल ट्रांसफ़ॉर्मेशन एपीआई का इस्तेमाल किया था.

  • ऐसे में, आपको रॉ इनपुट से विज़ुअल आउटपुट और फिर वापस रॉ इनपुट तक, दोनों तरफ़ से मैपिंग लागू करनी होगी.

  • इसे सही करना वाकई मुश्किल था. इस वजह से, अक्सर रनटाइम एक्सेप्शन की समस्या आती थी.

  • अब हमारे पास आउटपुट ट्रांसफ़ॉर्मेशन है. इसके बाद, यह फिर से TextFieldBuffer से काम करता है. इसलिए, आपको काफ़ी हद तक एक जैसा एपीआई मिलता है.

    @Stable
    public fun interface OutputTransformation {
        public fun TextFieldBuffer.transformOutput()
    }
  • इसलिए, फ़ोन नंबर के इस उदाहरण में, किसी खास इंडेक्स पर उन डैश को शामिल करने के लिए, आपको TextFieldBuffer के इंसर्ट फ़ंक्शन को कॉल करना होगा. इसके बाद, सिस्टम आपके लिए बिडाइरेक्शनल ऑफ़सेट मैपिंग का हिसाब लगाएगा.

    // Raw data untouched; formatting is purely visual
    outputTransformation = OutputTransformation {
        if (length > 6) {
            insert(6, "-")
        }
        if (length > 3) {
            insert(3, "-")
        }
    }
  • इससे मैन्युअल तरीके से हिसाब लगाने की ज़रूरत नहीं पड़ती और क्रैश की संख्या में काफ़ी कमी आती है.

TextFieldState को होस्ट और माइग्रेट करना

  • अब सबसे अहम बात यह है कि माइग्रेट कैसे करें और TextFieldState को कहां होस्ट करें?

  • आम तौर पर, इस्तेमाल का हर उदाहरण इनमें से किसी एक कैटगरी में आता है.

  • अगर ऐसा नहीं है, तो इसका मतलब है कि आपके पास कुछ अलग तरह का आर्किटेक्चर है. हालांकि, ये उदाहरण अब भी माइग्रेशन में आपकी मदद करेंगे.

  • ठीक है, तो सबसे आसान तरीका यह है कि TextFieldState को अपने कंपोज़ेबल के अंदर ही रखें, जहां वह टेक्स्ट फ़ील्ड मौजूद है.

    @Composable
    fun PromoCodeInput(onApply: (String) -> Unit) {
        // State hoisted locally inside the composable
        val state = rememberTextFieldState()
    
        OutlinedTextField(
            state = state,
            inputTransformation = InputTransformation.maxLength(6).allCaps(Locale.current),
            trailingIcon = { Button(onClick = { onApply(state.text.toString()) }) { }
        )
    }
  • ऐसा तब होता है, जब आपका टेक्स्ट फ़ील्ड एक ऐसा इनपुट फ़ील्ड हो जिसमें सभी ज़रूरी जानकारी मौजूद हो. उदाहरण के लिए, प्रोमो कोड वाला इनपुट फ़ील्ड. साथ ही, आपको उस टेक्स्ट का ऐक्सेस सिर्फ़ तब चाहिए, जब उपयोगकर्ता सबमिट बटन दबाए. क्या ऐसा ही है?

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

  • इसके लिए, आपको बस rememberTextFieldState को स्थानीय तौर पर कॉल करना होगा. साथ ही, rememberTextFieldState प्रोसेस बंद होने के बाद, स्टेट को वापस लाने में भी आपकी मदद करता है.

  • इसके बाद, फ़िल्टर करने के लिए इनपुट ट्रांसफ़ॉर्मेशन का इस्तेमाल किया जाता है. साथ ही, आपको उस स्टेट, TextFieldState ऑब्जेक्ट से टेक्स्ट मिलता है. इसके बाद, जब उपयोगकर्ता सबमिट करता है, तो उसे लैम्डा के ज़रिए पास किया जाता है.

  • ठीक है, अगर सिबलिंग यूज़र इंटरफ़ेस (यूआई) एलिमेंट, जैसे कि वर्ण काउंटर या सबमिट बटन को भी टेक्स्ट पढ़ना हो या उसमें बदलाव करना हो, तो क्या होगा?

  • इससे हमें एक ऐसा केस मिलता है जहां TextFieldState ऑब्जेक्ट को एक सामान्य कंपोज़ेबल फ़ंक्शन में होस्ट किया जाता है. यह एक सामान्य एंसेस्टर कंपोज़ेबल फ़ंक्शन होता है.

    @Composable
    fun PostCommentBar(onSubmit: (String) -> Unit) {
        // to share between UI components
        val commentState = rememberTextFieldState()
    
        Column {
            CommentTextField(commentState)
            Row {
                Text("${commentState.text.length} / 280")
                SubmitButton(commentState)
            }
        }
  • TextFieldState, स्नैपशॉट सिस्टम पर आधारित है. इसलिए, इसे पैरंट कॉम्पोनेंट से पास करने पर, पैरंट कॉम्पोनेंट फिर से कॉम्पोज़ नहीं होते.

  • सिर्फ़ उन जगहों पर बदलाव होगा जहां आपने राज्य का टेक्स्ट पढ़ा है.

  • ठीक है।

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

    @Composable
    fun PostEditor() {
    
        // Hoisting and wiring related pieces separately in the composable
        val textFieldState = rememberTextFieldState()
        val tagRanges = remember { mutableStateListOf<TextRange>() }
        val inputTransform = remember { TagInputTransformation(tagRanges) }
        val outputTransform = remember { TagOutputTransformation(tagRanges) }
    
        BasicTextField(
            state = textFieldState,
            inputTransformation = inputTransform,
            outputTransformation = outputTransform,
        )
    }
  • मान लें कि आपको इन-लाइन हैशटैग मैनेज करना है.

  • TextFieldState में रॉ टेक्स्ट और कर्सर की पोज़िशन होती है.

  • इनपुट ट्रांसफ़ॉर्मेशन, हैशटैग का पता लगाने के लिए ज़िम्मेदार होता है. साथ ही, इनपुट ट्रांसफ़ॉर्मेशन की मदद से, ऐटॉमिक टैग को मिटाया जा सकता है.

  • आउटपुट ट्रांसफ़ॉर्मेशन से, अतिरिक्त विज़ुअल स्टाइलिंग जोड़ी जा सकती है. साथ ही, हैशटैग को मिटाने में भी मदद मिल सकती है.

  • इन सभी को एक ही स्टेट होल्डर ऑब्जेक्ट में पैक किया जाता है. इसे TagEditorState कहा जाता है.

    @Stable
    class TagEditorState(val textFieldState: TextFieldState = TextFieldState()) {
    
        private val tagRanges = mutableStateListOf<TextRange>()
    
        // Transformations coordinate state internally
        val inputTransformation = TagInputTransformation(tagRanges)
        val outputTransformation = TagOutputTransformation(tagRanges)
    
        fun addTag(tag: String) { ... }
    
    }
  • इसके बाद, वे सभी इंटरमीडिएट कैलकुलेशन का फिर से इस्तेमाल कर सकते हैं. जैसे, इस मामले में tagRanges.

  • इसके बाद, इस स्टेट होल्डर ऑब्जेक्ट को अपने कंपोज़ेबल में पास करें.

    @Stable
    class TagEditorState(val textFieldState: TextFieldState = TextFieldState()) {}
    
    @Composable
    fun PostEditor(state: TagEditorState = rememberTagEditorState()) {
        BasicTextField(
            state = state.textFieldState,
            inputTransformation = state.inputTransformation,
            outputTransformation = state.outputTransformation,
        )
    }
  • ठीक है, एक और इस्तेमाल का उदाहरण तब होता है, जब आपके कारोबारी नियम को टाइप करते समय खोजने या कुछ जटिल पुष्टि के लिए, उस टेक्स्ट को ऐक्सेस करने की ज़रूरत होती है. इसके लिए, उस टेक्स्ट फ़ील्ड को अपने ViewModel में होस्ट करें.

  • यह ज़रूरी है कि आप म्यूटेबल टेक्स्ट फ़ील्ड ऑब्जेक्ट को इम्यूटेबल डेटा क्लास यूज़र इंटरफ़ेस (यूआई) स्टेट में न डालें. ऐसा इसलिए, क्योंकि इसे कॉपी करने के तरीके से कॉपी किया जाता है. इसके बाद, हर बदलाव पर StateFlow में भेजा जाता है.

    data class FormUiState(val username: TextFieldState)
  • TextFieldState, स्नैपशॉट सिस्टम की मदद से बनाया गया एक ऐसा ऑब्जेक्ट है जो लंबे समय तक काम करता है और जिसमें बदलाव किया जा सकता है. साथ ही, इसका रेफ़रंस ऑब्जेक्ट कभी नहीं बदलता.

  • इसलिए, आपके StateFlow को यह भी पता नहीं चलेगा कि उपयोगकर्ता इस समय टाइप कर रहा है, क्योंकि रेफ़रंस वही रहता है.

  • इससे भी खराब बात यह है कि अगर यूज़र इंटरफ़ेस (यूआई) की स्थिति का नया इंस्टेंस बनाया जाता है और नया TextFieldState असाइन किया जाता है, तो आपके इनपुट फ़ील्ड की सारी स्थिति मिट जाएगी.

  • जैसे, चुने गए टेक्स्ट, कर्सर की पोज़िशन या पहले जैसा करने/फिर से करने के इतिहास की जानकारी.

  • इसलिए, TextFieldState को सीधे तौर पर ViewModel की प्रॉपर्टी बनाएं.

    class FormViewModel : ViewModel() {
       val username = TextFieldState()
       val uiState: StateFlow<FormUiState> = ...
    }
  • यहां ViewModel नियम में यूज़र इंटरफ़ेस (यूआई) न होने की चिंता न करें. ऐसा इसलिए, क्योंकि इस मामले में TextFieldState, Android व्यू या कॉन्टेक्स्ट ऑब्जेक्ट या Compose लाइफ़साइकल का रेफ़रंस नहीं देता.

  • यह सिर्फ़ एक स्नैपशॉट-बैक्ड स्टेट होल्डर ऑब्जेक्ट है.

  • इसलिए, इस मामले में यह सिद्धांत के तौर पर StateFlow से अलग नहीं है.

  • एक बात और याद रखें कि ViewModel में rememberSaveable नहीं होता है. इसलिए, आपको टेक्स्ट फ़ील्ड के लिए TextFieldState.Saver को वायर करना होगा, ताकि प्रोसेस बंद होने पर भी टेक्स्ट फ़ील्ड की स्थिति सेव रहे और प्रोसेस के फिर से शुरू होने पर उसे वापस लाया जा सके.

    class FormViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
        val username: TextFieldState = savedStateHandle.saveable(
            key = "username",
            saver = TextFieldState.Saver,
            init = { TextFieldState() }
        )
       val uiState: StateFlow<FormUiState> = ...
    }
  • अब मान लीजिए कि आपकी टीम के पास मॉड्यूल के लिए सख्त नियम हैं, जहां ViewModels सिर्फ़ Kotlin मॉड्यूल में मौजूद होते हैं और आपके पास compose-foundation डिपेंडेंसी को लाने का विकल्प नहीं है?

  • या फिर, मान लें कि आपको किसी जटिल लेगसी स्क्रीन को माइग्रेट करना है और आपके पास आज यूज़र इंटरफ़ेस (यूआई) की स्थिति को अपडेट करने या उसे रीफ़ैक्टर करने का समय नहीं है?

  • ऐसे में, नियमों का पालन करने वाला तरीका अपनाएं.

  • इसलिए, TextFieldState को कंपोज़ेबल में उस टेक्स्ट फ़ील्ड के लिए, एकमात्र भरोसेमंद सोर्स के तौर पर रखा जाता है.

  • इसके बाद, टेक्स्ट में किए गए बदलावों को अपने मौजूदा ViewModel में वापस भेजने के लिए, snapshotFlow के साथ LaunchedEffect का इस्तेमाल किया जाता है.

    @Composable
    fun LoginScreen(viewModel: LoginViewModel) {
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        // 1. Hoist TextFieldState in UI layer
        val usernameState = rememberTextFieldState(initialText = uiState.initialUsername)
    
        // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
        LaunchedEffect(usernameState, viewModel) {
            snapshotFlow { usernameState.text.toString() }
                .drop(1)
                .collect { viewModel.onUsernameChanged(it) }
        }
    
        BasicTextField(state = usernameState)
    }
  • साथ ही, आपका ViewModel, Compose से मुक्त रहता है. यूज़र इंटरफ़ेस (यूआई) की स्टेट प्रॉपर्टी, हर बदलाव पर टेक्स्ट फ़ील्ड में फ़ीडबैक नहीं देती हैं. इसलिए, आपको अब भी एसिंक्रोनस राउंड ट्रिप की गड़बड़ियां नहीं मिलती हैं.

  • हां, और यहां एक आम जाल से सावधान रहें.

    @Composable
    fun LoginScreen(viewModel: LoginViewModel) {
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        // 1. Hoist TextFieldState in UI layer
        val usernameState = rememberTextFieldState(initialText = uiState.initialUsername)
    
        // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
        LaunchedEffect(usernameState, viewModel) {
            snapshotFlow { usernameState.text.toString() }
                .drop(1) // 👈 CRITICAL: Avoids firing on initial composition or rotation
                .collect { viewModel.onUsernameChanged(it) }
        }
    
        BasicTextField(state = usernameState)
    }
  • ज़्यादातर मामलों में, आपको पहले इवेंट को छोड़ना होगा.

  • इससे, डुप्लीकेट सर्च क्वेरी को बार-बार होने से रोका जा सकेगा. इसके अलावा, आपके ViewModel को शुरुआती वैल्यू से ओवरराइट होने से भी रोका जा सकेगा.

  • ठीक है, तो अगर आपके लॉजिक को स्टेट में उन बदलावों को फ़ीडबैक करने के लिए फ़ीडबैक देना है, तो उदाहरण के लिए, जब उपयोगकर्ता सबमिट बटन दबाता है और आपको टेक्स्ट को मिटाना है, तो ViewModel के ViewModel में खाली स्ट्रिंग सेट नहीं की जा सकती.

    @Composable
    fun LoginScreen(viewModel: LoginViewModel) {
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        // 1. Hoist TextFieldState in UI layer
        // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
        ...
    
        // 3. Handle discrete ViewModel events (e.g. clear after login)
        LaunchedEffect(viewModel) {
            viewModel.clearUsernameEvent.collect {
                usernameState.clearText()
            }
        }
        BasicTextField(state = usernameState)
    }
  • इसके बजाय, आपको साफ़ तौर पर टेक्स्ट इवेंट को एमिट करना होगा. इसके बाद, TextFieldState ऑब्जेक्ट को होल्ड करने वाला कंपोज़ेबल फ़ंक्शन, उस ऑब्जेक्ट पर clearText को कॉल करेगा.

  • इस बदलाव को समझने में आपकी मदद करने के लिए, हमने माइग्रेट करने से जुड़ी यह गाइड तैयार की है.

  • इसे इस लिंक से ऐक्सेस किया जा सकता है.

  • हम कुछ और एपीआई पर भी काम कर रहे हैं, ताकि माइग्रेशन और TextFieldState में होने वाले सामान्य बदलावों को आसानी से देखा जा सके. इसलिए, हमारे साथ बने रहें.

फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना

  • हमने अब तक जो भी कवर किया है, जैसे कि TextFieldState, TextFieldBuffer, इनपुट और आउटपुट ट्रांसफ़ॉर्मेशन, उन्हें सिर्फ़ सादे टेक्स्ट इनपुट के लिए नहीं बनाया गया है.

  • यह फ़ॉर्मैट किए गए टेक्स्ट में बदलाव करने के लिए, हमारे नए एपीआई सेट का आधार है.

  • अब मैं हाओयू को बुलाना चाहूंगा, जो आपको इस बारे में ज़्यादा जानकारी देंगे.

  • ठीक है।

  • धन्यवाद.

  • सभी को नमस्ते।

  • इसके बाद, मैं उन नए एपीआई के बारे में बात करूंगा जिन्हें हमने फ़ॉर्मैट किए गए टेक्स्ट में बदलाव करने के लिए पेश किया है.

  • इसलिए, नए एपीआई के बारे में जानने से पहले, आइए स्टैटिक टेक्स्ट में स्टाइलिंग के काम करने के तरीके को समझते हैं.

  • इसलिए, आप में से ज़्यादातर लोगों को AnnotatedString के बारे में पहले से ही अच्छी तरह से पता है.

    val styledText = buildAnnotatedString {
        append("Jetpack Compose ")
        // 1. Blue & Bold on "Jetpack" [0, 7)
        addStyle(
            style = SpanStyle(
                color = Color.Blue,
                fontWeight = FontWeight.Bold
            ),
            start = 0,
            end = 7
        )
    }
    // Render static formatted text
    Text(text = styledText)
  • यह स्टैटिक टेक्स्ट को स्टाइल करने का स्टैंडर्ड तरीका है.

  • हम buildAnnotatedString का इस्तेमाल करते हैं और addStyle को कॉल करते हैं, ताकि दी गई कुछ रेंज पर SpanStyle या ParagraphStyle को लागू किया जा सके.

  • यह स्टैटिक टेक्स्ट के लिए अच्छी तरह से काम करता है.

  • इसलिए, जब हम फ़ॉर्मैट किए गए टेक्स्ट में बदलाव करने की सुविधा पर काम कर रहे थे, तो हमारा पहला विचार यह था कि क्या हम AnnotatedString को बदलाव करने लायक बना सकते हैं?

  • हालांकि, हमें जल्द ही एहसास हुआ कि यह इतना आसान नहीं है.

  • AnnotatedString, डेटा का ऐसा स्ट्रक्चर है जिसमें बदलाव नहीं किया जा सकता.

  • लेकिन एडिटिंग की दुनिया में, उपयोगकर्ता लगातार टाइप करता रहता है.

  • हर कीस्ट्रोक के लिए, हमें एक नया AnnotatedString ऑब्जेक्ट असाइन करना पड़ता है.

  • हर AnnotatedString में कई AnnotatedString.Range होते हैं.

  • इसलिए, उन ऑब्जेक्ट को भी फिर से बनाना होगा.

  • यह बहुत अच्छी तरह से स्केल नहीं होता और इसकी वजह से, ऐलोकेशन ओवरहेड काफ़ी ज़्यादा होता है.

  • इसलिए, यह उदाहरण देखें.

  • जब हम बीच में कोई शब्द डालते हैं, तो उसके बाद की हर स्टाइल अपनी रेंज बदल लेती है और उसे फिर से असाइन किया जाता है.

  • हालांकि, अगर आपके पास कई स्टाइल हैं, तो यह तरीका काम नहीं करता.

  • इसलिए, हमने TextFieldBuffer में स्टाइलिंग के लिए खास एपीआई जोड़े हैं. इनके बारे में अनास्तासिया ने पहले बताया था.

  • ये AnnotatedString में मौजूद स्टाइलिंग एपीआई से काफ़ी मिलते-जुलते हैं.

  • आइए, जानते हैं कि ये कैसे काम करते हैं.

  • इसलिए, AnnotatedString.Builder की तरह ही, TextFieldBuffer में अब addStyle है, ताकि किसी रेंज पर SpanStyle या ParagraphStyle लागू किया जा सके.

    fun addStyle(spanStyle: SpanStyle, start: Int, end: Int)
    
    fun addStyle(paragraphStyle: ParagraphStyle, start: Int, end: Int)
    
    fun addStyle(spanStyle: SpanStyle, start: Int, end: Int, expandPolicy: ExpandPolicy): TrackedRange<SpanStyle>
    
    fun addStyle(paragraphStyle: ParagraphStyle, start: Int, end: Int, expandPolicy: ExpandPolicy): TrackedRange<ParagraphStyle>
  • आपके पास ExpandPolicy पैरामीटर पास करने का विकल्प भी होता है. इससे यह कंट्रोल किया जाता है कि स्टाइल की सीमाओं पर टेक्स्ट टाइप करने पर, स्टाइल कैसा व्यवहार करे.

  • हम इस बारे में थोड़ी देर में बात करेंगे.

  • सबसे पहले, आइए जानते हैं कि स्टाइल ट्रैकिंग की सुविधा कैसे काम करती है.

    val state = TextFieldState("The quick fox jumps")
    
    state.edit {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            start = 4,
            end = 13
        )
        insert(0, "Look! ")
        insert(16, "brown ")
        delete(16, 22)
        delete(10, 19)
    }
  • TextFieldBuffer में कोई स्टाइल जोड़ने के बाद, टेक्स्ट में बदलाव करने पर उसकी रेंज अपने-आप अपडेट हो जाती है.

  • इस उदाहरण में, हम कुछ टेक्स्ट को बोल्ड करने से शुरुआत करते हैं.

  • जब इसके पहले टेक्स्ट डाला जाता है, तो स्टाइल रेंज आगे की ओर शिफ़्ट हो जाती है.

  • इसके अंदर टेक्स्ट डालने पर, टेक्स्ट की रेंज बढ़ जाती है.

  • साथ ही, रेंज के अंदर मौजूद टेक्स्ट को मिटाने पर, स्टाइल रेंज भी उसी हिसाब से छोटी हो जाती है.

  • अगर स्टाइल रेंज में मौजूद पूरा टेक्स्ट मिटा दिया जाता है, तो स्टाइल छोटा हो जाता है और बफ़र से अपने-आप हट जाता है.

  • अब सवाल यह है कि अगर टेक्स्ट को स्टाइल रेंज की सीमाओं पर ही डाला जाए, तो क्या होगा?

  • क्या नए टेक्स्ट पर स्टाइल लागू हुई है या नहीं?

  • इसलिए, आपको इस पर कंट्रोल देने के लिए, हमने ExpandPolicy एपीआई लॉन्च किए हैं.

  • आइए, इन पर नज़र डालते हैं.

  • पहला है ExpandPolicy.InsideOnly.

    val state = TextFieldState("The fox")
    
    state.edit {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            start = 4,
            end = 7,
            policy = ExpandPolicy.InsideOnly
        )
        insert(4, "quick ")
        insert(length, " jumps")
  • इस मोड में, टेक्स्ट की सीमा पर टेक्स्ट डालने से स्टाइल रेंज नहीं बढ़ेगी.

  • यहां हम सबसे पहले, बोल्ड रेंज लागू करते हैं.

  • अगर हम बोल्ड रेंज की शुरुआत में टेक्स्ट डालते हैं, तो स्टाइल रेंज शिफ़्ट हो जाती है, लेकिन बड़ी नहीं होती.

  • इसी तरह, अगर हम बोल्ड रेंज के आखिर में टेक्स्ट डालते हैं, तो स्टाइल रेंज भी नहीं बढ़ती.

  • यहां एक डेमो दिया गया है, जिसमें दिखाया गया है कि यह एडिटर में कैसे काम करता है.

  • इसके बाद, ExpandPolicy.AtStart है.

    val state = TextFieldState("The fox")
    
    state.edit {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            start = 4,
            end = 7,
            policy = ExpandPolicy.AtStart
        )
        insert(4, "quick ")
  • जैसा कि नाम से पता चलता है, रेंज की शुरुआत में टेक्स्ट डालने से स्टाइल रेंज बढ़ जाती है.

  • इसलिए, जब हम टेक्स्ट डालते हैं, तो स्टाइल रेंज का दायरा बढ़ जाता है, ताकि नए टेक्स्ट को कवर किया जा सके.

  • यहां एक और डेमो दिया गया है, जिसमें दिखाया गया है कि यह सुविधा कैसे काम करती है.

  • तीसरा है ExpandPolicy.AtEnd.

    val state = TextFieldState("The fox")
    
    state.edit {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            start = 4,
            end = 7,
            policy = ExpandPolicy.AtEnd
        )
        insert(length, " jumps")
  • इसलिए, हम रेंज के आखिर में टेक्स्ट शामिल कर रहे हैं, ताकि स्टाइल रेंज को बड़ा किया जा सके.

  • इसलिए, यह एडिटर के लिए सबसे सामान्य सेटिंग भी है.

  • यहां एक डेमो दिया गया है, जिसमें बताया गया है कि यह कैसे काम करता है.

  • आखिर में, ExpandPolicy.AtBoth है. यह स्टाइल को तब बड़ा करता है, जब टेक्स्ट को रेंज की शुरुआत और आखिर में डाला जाता है.

    val state = TextFieldState("The fox")
    
    state.edit {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            start = 4,
            end = 7,
            policy = ExpandPolicy.AtBoth
        )
        insert(4, "quick ")
     insert(length, " jumps")
    }
  • इसलिए, इसका एक और डेमो.

  • हमने अब तक स्टाइलिंग लागू करने वाले सभी एपीआई के बारे में बताया है.

  • इसके अलावा, हमने getSpanStyles और getParagraphStyles जैसे नए क्वेरी एपीआई भी लॉन्च किए हैं. इनकी मदद से, किसी रेंज में लागू की गई स्टाइल को फ़ेच किया जा सकता है.

    // Outside edits. ReadOnly API in TextFieldState.
    val textFieldState.styles: TextFieldTextStyles
    fun TextFieldTextStyles.getSpanStyle(range: TextRange): List<AnnotatedString.Range<SpanStyle>>
    
    // Inside edits, returned TrackedRange
    fun TextFieldBuffer.getSpanStyles(range: TextRange): List<TrackedRange<SpanStyle>>
  • यहां क्वेरी एपीआई के दो वर्शन दिए गए हैं.

  • TextFieldState को, स्टाइल क्वेरी एपीआई रीड-ओनली मोड में उपलब्ध होंगे.

  • ये ऐसे AnnotatedString.Range ऑब्जेक्ट की सूची दिखाते हैं जिनमें बदलाव नहीं किया जा सकता.

  • हालांकि, TextFieldBuffer में, getSpanStyles ने नए TrackedRange ऑब्जेक्ट की सूची दिखाई. इनके बारे में मैं बाद में बताऊंगी.

  • इसलिए, सभी स्टाइल क्वेरी, क्रम के हिसाब से काम करती हैं.

  • ये स्टाइल रेंज को उसी क्रम में दिखाते हैं जिस क्रम में स्टाइल लागू की गई है.

  • इससे आपको AnnotatedString की तरह ही, किसी मौजूदा स्टाइल के ऊपर दूसरी स्टाइल लागू करने की सुविधा मिलती है, ताकि पहले वाली स्टाइल को बदला जा सके.

  • इसके बाद, TrackedRange API है.

    var TrackedRange<*>.textRange: TextRange
    
    var TrackedRange<*>.expandPolicy: ExpandPolicy
    
    var TrackedRange<SpanStyle>.spanStyle: SpanStyle
    
    var TrackedRange<ParagraphStyle>.paragraphStyle: ParagraphStyle
    
    val TrackedRange<*>.isValid: Boolean
    
    fun removeStyle(trackedRange: TrackedRange<*>): Boolean
  • TextFieldBuffer में addStyle या getSpanStyles एपीआई से TrackedRange ऑब्जेक्ट दिखाए जाते हैं.

  • इसे बफ़र में स्टाइल रेंज के लाइव हैंडल के तौर पर देखें.

  • इसका इस्तेमाल करके, टेक्स्ट की रेंज, ExpandPolicy या स्टाइल को अपडेट किया जा सकता है.

  • इसमें एक isValid प्रॉपर्टी भी होती है. इससे यह पता चलता है कि स्टाइल रेंज अब भी TextFieldBuffer में है या नहीं.

  • ऐसा इसलिए होता है, क्योंकि बदलाव करने के दौरान स्टाइल को हटाया जा सकता है. ऐसा तब होता है, जब बदलाव करने के दौरान स्टाइल को छोटा कर दिया जाता है या removeStyle उसे जान-बूझकर हटा देता है.

  • चलिए, अब देखते हैं कि असल ज़िंदगी में इन एपीआई का इस्तेमाल कैसे किया जाता है.

WYSIWYG एडिटर बनाना

  • पहला इस्तेमाल, WYSIWYG एडिटर बनाने के लिए किया जाता है.

  • इस उदाहरण में, हमने बोल्ड बटन की सुविधा लागू की है.

  • बोल्ड बटन पर क्लिक करने से, चुने गए टेक्स्ट पर बोल्ड स्टाइल लागू हो जाती है.

  • चलिए, अब कोड देखते हैं.

  • इसलिए, जब उपयोगकर्ता बोल्ड बटन पर टैप करता है, तो हम सबसे पहले चुने गए टेक्स्ट पर getSpanStyles को कॉल करते हैं, ताकि ओवरलैप होने वाली सभी स्टाइल का पता लगाया जा सके.

    fun TextFieldBuffer.toggleBoldSelection() {
        if (selection.collapsed) return
        val boldSpans = getSpanStyles(selection)
    
        if (!isFullyBold(boldSpans, selection)) {
            addStyle(
                SpanStyle(fontWeight = FontWeight.Bold),
                selection,
                ExpandPolicy.AtEnd
            )
        } else {
            unboldSelection(boldSpans.first())
        }
    }
  • इसके बाद, हम यह जांच करते हैं कि चुने गए पूरे टेक्स्ट को पहले से ही बोल्ड किया गया है या नहीं.

  • यहां, चुना गया शब्द बोल्ड नहीं है.

  • चूंकि यह बोल्ड नहीं है, इसलिए हम चुनी गई रेंज में addStyle.

  • हम ExpandPolicy.AtEnd पास करते हैं, ताकि उपयोगकर्ता इस शब्द के ठीक बाद बोल्ड टेक्स्ट टाइप करना जारी रख सके.

  • अब देखते हैं कि अगर चुनी गई रेंज पहले से ही बोल्ड है, तो क्या होगा.

    fun TextFieldBuffer.unboldSelection(
        boldSpan: TrackedRange<SpanStyle>
    ) {
        val hasBoldBefore = boldSpan.start < selection.min
        val hasBoldAfter = boldSpan.end > selection.max
    
        when {
            hasBoldBefore ->
                boldSpan.range = TextRange(range.start, selection.min)
            hasBoldAfter ->
                boldSpan.range = TextRange(selection.max, range.end)
            hasBoldBefore && hasBoldAfter -> {
                val oldEnd = boldSpan.end
                boldSpan.range = TextRange(boldSpan.start, selection.min)
                addStyle(
                    boldSpan.spanStyle,
                    start = selection.max,
                    end = oldEnd
                )
            }
            else -> removeStyle(range)
        }
    }
  • ऐसे में, हमें चुनी गई रेंज से बोल्ड स्टाइल हटाना होगा.

  • यहां, उपयोगकर्ता ने जिस शब्द को चुना है वह बोल्ड किए गए लंबे वाक्यांश के अंदर है.

  • बोल्ड स्टाइल, चुने गए शब्द से पहले लागू हो जाती है. इसलिए, हमें स्टाइल की रेंज को अपडेट करना होगा. इससे, चुने गए शब्द को बोल्ड नहीं किया जाएगा.

  • इसके लिए, हम TrackedRange एपीआई का इस्तेमाल कर सकते हैं.

  • इस मामले में, इस स्टाइल को मिटाने और फिर से बनाने के बजाय, हम बोल्ड स्पैन रेंज को इंडेक्स 10 पर चुने गए टेक्स्ट से पहले खत्म होने के लिए अडजस्ट कर सकते हैं.

  • ज़ाहिर है कि यह बहुत ही आसान तरीका है.

  • पूरा डेमो देखने के लिए, Compose के सैंपल की रिपॉज़िटरी देखें.

  • इसके बाद, आइए पैटर्न के हिसाब से स्टाइलिंग करने का तरीका देखते हैं.

  • इस डेमो में, जब उपयोगकर्ता किसी शब्द के आस-पास मार्कडाउन ऐस्टरिस्क टाइप करता है, तो एडिटर टाइप करते ही उसे बोल्ड टेक्स्ट में बदल देता है.

  • अब देखते हैं कि InputTransformation का इस्तेमाल करके इसे कैसे लागू किया जाता है.

    object MarkdownBoldTransformation : InputTransformation {
        val regex = Regex("""**(.*?)**""")
    
        override fun TextFieldBuffer.transformInput() {
            val matches = regex.findAll(asCharSequence()).toList()
    
            for (match in matches) {
                val range = match.range
                val content = match.groupValues[1]
    
                replace(range.first, range.last + 1, content)
                addStyle(
                    SpanStyle(fontWeight = FontWeight.Bold),
                    start = range.first,
                    end = range.first + content.length,
                    policy = ExpandPolicy.atEnd
                )
            }
        }
    }
  • इनपुट फ़िल्टर को बदलने के लिए, हम बटन पर मौजूद सभी मैचिंग पैटर्न ढूंढते हैं.

  • हर मैच के लिए, हम डबल ऐस्टरिस्क के अंदर मौजूद कॉन्टेंट को एक्सट्रैक्ट करते हैं.

  • इस उदाहरण में, यह शब्द "fox" है.

  • इसके बाद, हम ऐस्टरिस्क हटा देते हैं और मैच होने वाले पैटर्न को उसके कॉन्टेंट से बदल देते हैं.

  • आखिर में, हम कॉन्टेंट टेक्स्ट पर बोल्ड स्टाइल लागू करने के लिए, addStyle को बोल्ड पर कॉल करते हैं.

  • बस याद रखें कि InputTransformation, टेक्स्ट फ़ील्ड की आखिरी स्थिति में सबमिट हो जाएगा.

  • इसका मतलब है कि बोल्ड स्टाइल, राज्य पर हमेशा लागू रहती है.

  • आखिर में, खोज के नतीजों को हाइलाइट करने का उदाहरण देखते हैं.

  • इस उदाहरण में, जब उपयोगकर्ता कोई सर्च क्वेरी टाइप करता है, तो टेक्स्ट फ़ील्ड में मौजूद मिलते-जुलते शब्दों को हाइलाइट किया जाता है. हालांकि, टेक्स्ट फ़ील्ड की स्थिति में कोई बदलाव नहीं किया जाता.

  • अब देखते हैं कि इसे OutputTransformation में कैसे लागू किया जाता है.

  • खोज के लिए इस्तेमाल किए गए किसी शब्द को हाइलाइट करते समय, टेक्स्ट की स्थिति में कभी भी बदलाव न करें.

  • इस इस्तेमाल के उदाहरण को लागू करने के लिए, OutputTransformation सबसे सही जगह है.

  • चलिए, अब इसे लागू करने का तरीका देखते हैं.

  • हम सबसे पहले, हाइलाइट किए गए शब्दों को खोजते हैं और उनसे मिलते-जुलते सभी नतीजे ढूंढते हैं.

    class SearchHighlightTransformation(
        val query: MutableState<String>
    ) : OutputTransformation {
        override fun TextFieldBuffer.transformOutput() {
            val search = query.value
            if (search.isEmpty()) return
            val text = asCharSequence().toString()
            var index = text.indexOf(search)
    
            while (index != -1) {
                addStyle(
                    SpanStyle(
                        background = Color.Yellow,
                        color = Color.Black
                    ),
                    start = index,
                    end = index + search.length
                )
                index = text.indexOf(search, index + search.length)
            }
        }
    }
  • ध्यान दें कि क्वेरी को बदला जा सकता है.

  • ऐसा इसलिए होता है, क्योंकि OutputTransformation, स्नैपशॉट के बारे में जानकारी रखता है.

  • जब यह म्यूटेबल स्टेट अपडेट होती है, तो OutputTransformation अपने-आप फिर से लागू हो जाता है.

  • इसके बाद, जब हम हर मैच को लूप करते हैं और मैच रेंज पर पीले रंग की हाइलाइट बनाने के लिए addStyle को कॉल करते हैं.

  • OutputTransformation सिर्फ़ विज़ुअल टेक्स्ट में बदलाव करता है. इससे ओरिजनल टेक्स्ट में कोई बदलाव नहीं होता.

  • ठीक है, हमारे पास इस्तेमाल के इतने ही उदाहरण हैं.

  • आपको याद दिला दें कि पहले दिखाए गए सभी एपीआई, Compose 1.12 से उपलब्ध हैं.

आने वाले समय में उपलब्ध होने वाली मार्कअप टेक्स्ट लाइब्रेरी

  • आखिर में, मैं आपको उस नए प्रोजेक्ट की झलक दिखाना चाहता हूं जिस पर हम अभी काम कर रहे हैं.

  • यह Jetpack Compose के लिए एक मार्कअप टेक्स्ट लाइब्रेरी है. इसमें Markdown जैसे रिच टेक्स्ट फ़ॉर्मैट इस्तेमाल किए जा सकते हैं.

  • हम जिन सुविधाओं को सपोर्ट कर रहे हैं उनमें से एक सबसे दिलचस्प सुविधा है, इंक्रीमेंटल पार्सिंग और इंक्रीमेंटल रेंडरिंग.

  • इसलिए, यह लाइव एडिटिंग या मार्कडाउन स्ट्रीमिंग के लिए बहुत सही है.

  • यहां हमारे वर्किंग प्रोटोटाइप का रिकॉर्ड है. इसमें आपको फ़ेड होते हुए जो हिस्से दिख रहे हैं वे यूज़र इंटरफ़ेस (यूआई) में अपडेट किए जा रहे हिस्से हैं.

  • अगर आपको इसमें दिलचस्पी है या आपके ऐप्लिकेशन में इसका कोई इस्तेमाल है, तो कृपया हमसे संपर्क करें.

  • आप सभी का धन्यवाद.

कोड

कोड स्निपेट

"Compose में टेक्स्ट एडिटर के बारे में जानकारी" में 00:29 पर Kotlin स्निपेट
TextField(state = rememberTextFieldState())
"Compose में टेक्स्ट में बदलाव करने के बारे में जानकारी" में 00:29 पर Kotlin स्निपेट
OutlinedTextField(state = rememberTextFieldState())
"Compose में टेक्स्ट एडिटर के बारे में जानकारी" में 00:34 पर Kotlin स्निपेट
BasicTextField(state = rememberTextFieldState())
"Compose में टेक्स्ट में बदलाव करने के बारे में जानकारी" में 00:44 पर Kotlin स्निपेट
SecureTextField(state = rememberTextFieldState())
"Compose में टेक्स्ट एडिटर के बारे में जानकारी" में 00:58 पर Kotlin स्निपेट
@Composable
public fun BasicTextField(
    state: TextFieldState,
)
"Compose में टेक्स्ट एडिटिंग के बारे में जानकारी" में 01:28 पर Kotlin स्निपेट
@Composable
public fun BasicTextField(
    value: String,
    onValueChange: (String) -> Unit
)

@Composable
public fun BasicTextField(
    value: TextFieldValue,
    onValueChange: (TextFieldValue) -> Unit
"Introducing TextFieldState" में 04:01 पर Kotlin स्निपेट
// Often generated by AI agents:
state.edit { insert(5, ",") }
state.edit { delete(12, 13) }
state.edit { replace(0, 5, "goodbye") }
state.edit { selection = TextRange(9, 16) }
"पेश है TextFieldState" में 04:09 पर Kotlin स्निपेट
// Batch all operations in a single atomic edit:
state.edit {
insert(5, ",")
delete(12, 13)
replace(0, 5, "goodbye")
selection = TextRange(9, 16)
}
"Introducing TextFieldState" में 04:25 पर Kotlin स्निपेट
fun TextFieldState.setTextAndPlaceCursorAtEnd(text: String) {
edit {
replace(0, length, text)
placeCursorAtEnd()
}
}
"Introducing TextFieldState" में 04:38 पर Kotlin स्निपेट
fun TextFieldState.clearText() {
edit {
delete(0, length)
placeCursorAtEnd()
}
}
"इनपुट फ़िल्टर करना और फ़ॉर्मैट करना" में 04:55 पर Kotlin स्निपेट
onValueChange { newString ->
  // full diffing new String vs old String
}
"इनपुट फ़िल्टर करना और फ़ॉर्मैट करना" में 05:17 पर Kotlin स्निपेट
@Stable
public fun interface InputTransformation {
    public fun TextFieldBuffer.transformInput()
}
"इनपुट फ़िल्टर करना और फ़ॉर्मैट करना" में 05:33 पर Kotlin स्निपेट
// Intercept and filter input during typing
inputTransformation = InputTransformation {
    val text = asCharSequence()
    if (!text.all { it.isDigit() }) {
        // Reject non-digit characters
        revertAllChanges()
    }
}
"इनपुट फ़िल्टर करना और फ़ॉर्मैट करना" में 06:48 पर Kotlin स्निपेट
@Stable
public fun interface OutputTransformation {
    public fun TextFieldBuffer.transformOutput()
}
"इनपुट फ़िल्टर करना और फ़ॉर्मैट करना" में 06:58 पर Kotlin स्निपेट
// Raw data untouched; formatting is purely visual
outputTransformation = OutputTransformation {
    if (length > 6) {
        insert(6, "-")
    }
    if (length > 3) {
        insert(3, "-")
    }
}
"TextFieldState को होस्ट और माइग्रेट करना" में 07:51 पर Kotlin स्निपेट
@Composable
fun PromoCodeInput(onApply: (String) -> Unit) {
    // State hoisted locally inside the composable
    val state = rememberTextFieldState()

    OutlinedTextField(
        state = state,
        inputTransformation = InputTransformation.maxLength(6).allCaps(Locale.current),
        trailingIcon = { Button(onClick = { onApply(state.text.toString()) }) { }
    )
}
"TextFieldState को होस्ट और माइग्रेट करना" में 08:58 पर Kotlin स्निपेट
@Composable
fun PostCommentBar(onSubmit: (String) -> Unit) {
    // to share between UI components
    val commentState = rememberTextFieldState()

    Column {
        CommentTextField(commentState)
        Row {
            Text("${commentState.text.length} / 280")
            SubmitButton(commentState)
        }
    }
"TextFieldState को होस्ट और माइग्रेट करना" में 09:27 पर Kotlin स्निपेट
@Composable
fun PostEditor() {

    // Hoisting and wiring related pieces separately in the composable
    val textFieldState = rememberTextFieldState()
    val tagRanges = remember { mutableStateListOf<TextRange>() }
    val inputTransform = remember { TagInputTransformation(tagRanges) }
    val outputTransform = remember { TagOutputTransformation(tagRanges) }

    BasicTextField(
        state = textFieldState,
        inputTransformation = inputTransform,
        outputTransformation = outputTransform,
    )
}
"TextFieldState को होस्ट और माइग्रेट करना" में 10:12 पर Kotlin स्निपेट
@Stable
class TagEditorState(val textFieldState: TextFieldState = TextFieldState()) {

    private val tagRanges = mutableStateListOf<TextRange>()

    // Transformations coordinate state internally
    val inputTransformation = TagInputTransformation(tagRanges)
    val outputTransformation = TagOutputTransformation(tagRanges)

    fun addTag(tag: String) { ... }

}
"TextFieldState को होस्ट और माइग्रेट करना" में 10:30 पर Kotlin स्निपेट
@Stable
class TagEditorState(val textFieldState: TextFieldState = TextFieldState()) {}

@Composable
fun PostEditor(state: TagEditorState = rememberTagEditorState()) {
    BasicTextField(
        state = state.textFieldState,
        inputTransformation = state.inputTransformation,
        outputTransformation = state.outputTransformation,
    )
}
"TextFieldState को होस्ट और माइग्रेट करना" वीडियो में 10:51 पर Kotlin स्निपेट
data class FormUiState(val username: TextFieldState)
"TextFieldState को होस्ट और माइग्रेट करना" वीडियो में 11:38 पर Kotlin स्निपेट
class FormViewModel : ViewModel() {
   val username = TextFieldState()
   val uiState: StateFlow<FormUiState> = ...
}
"TextFieldState को होस्ट और माइग्रेट करना" में 12:09 पर Kotlin स्निपेट
class FormViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    val username: TextFieldState = savedStateHandle.saveable(
        key = "username",
        saver = TextFieldState.Saver,
        init = { TextFieldState() }
    )
   val uiState: StateFlow<FormUiState> = ...
}
"TextFieldState को होस्ट और माइग्रेट करना" में 12:55 पर Kotlin स्निपेट
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 1. Hoist TextFieldState in UI layer
    val usernameState = rememberTextFieldState(initialText = uiState.initialUsername)

    // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
    LaunchedEffect(usernameState, viewModel) {
        snapshotFlow { usernameState.text.toString() }
            .drop(1)
            .collect { viewModel.onUsernameChanged(it) }
    }

    BasicTextField(state = usernameState)
}
"TextFieldState को होस्ट और माइग्रेट करना" में 13:21 पर Kotlin स्निपेट
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 1. Hoist TextFieldState in UI layer
    val usernameState = rememberTextFieldState(initialText = uiState.initialUsername)

    // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
    LaunchedEffect(usernameState, viewModel) {
        snapshotFlow { usernameState.text.toString() }
            .drop(1) // 👈 CRITICAL: Avoids firing on initial composition or rotation
            .collect { viewModel.onUsernameChanged(it) }
    }

    BasicTextField(state = usernameState)
}
"TextFieldState को होस्ट और माइग्रेट करना" में 13:39 पर Kotlin स्निपेट
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // 1. Hoist TextFieldState in UI layer
    // 2. Bridge UI -> ViewModel, one-way only (eliminates cursor jumps/dropped chars)
    ...

    // 3. Handle discrete ViewModel events (e.g. clear after login)
    LaunchedEffect(viewModel) {
        viewModel.clearUsernameEvent.collect {
            usernameState.clearText()
        }
    }
    BasicTextField(state = usernameState)
}
"Formatted and rich text editing" में 15:09 पर Kotlin स्निपेट
val styledText = buildAnnotatedString {
    append("Jetpack Compose ")
    // 1. Blue & Bold on "Jetpack" [0, 7)
    addStyle(
        style = SpanStyle(
            color = Color.Blue,
            fontWeight = FontWeight.Bold
        ),
        start = 0,
        end = 7
    )
}
// Render static formatted text
Text(text = styledText)
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 16:36 पर Kotlin स्निपेट
fun addStyle(spanStyle: SpanStyle, start: Int, end: Int)

fun addStyle(paragraphStyle: ParagraphStyle, start: Int, end: Int)

fun addStyle(spanStyle: SpanStyle, start: Int, end: Int, expandPolicy: ExpandPolicy): TrackedRange<SpanStyle>

fun addStyle(paragraphStyle: ParagraphStyle, start: Int, end: Int, expandPolicy: ExpandPolicy): TrackedRange<ParagraphStyle>
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 16:57 पर Kotlin स्निपेट
val state = TextFieldState("The quick fox jumps")

state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        start = 4,
        end = 13
    )
    insert(0, "Look! ")
    insert(16, "brown ")
    delete(16, 22)
    delete(10, 19)
}
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 17:52 पर Kotlin स्निपेट
val state = TextFieldState("The fox")

state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        start = 4,
        end = 7,
        policy = ExpandPolicy.InsideOnly
    )
    insert(4, "quick ")
    insert(length, " jumps")
"Formatted and rich text editing" में 18:22 पर Kotlin स्निपेट
val state = TextFieldState("The fox")

state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        start = 4,
        end = 7,
        policy = ExpandPolicy.AtStart
    )
    insert(4, "quick ")
"Formatted and rich text editing" में 18:40 पर Kotlin स्निपेट
val state = TextFieldState("The fox")

state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        start = 4,
        end = 7,
        policy = ExpandPolicy.AtEnd
    )
    insert(length, " jumps")
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 18:56 पर Kotlin स्निपेट
val state = TextFieldState("The fox")

state.edit {
    addStyle(
        SpanStyle(fontWeight = FontWeight.Bold),
        start = 4,
        end = 7,
        policy = ExpandPolicy.AtBoth
    )
    insert(4, "quick ")
 insert(length, " jumps")
}
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 19:14 पर Kotlin स्निपेट
// Outside edits. ReadOnly API in TextFieldState.
val textFieldState.styles: TextFieldTextStyles
fun TextFieldTextStyles.getSpanStyle(range: TextRange): List<AnnotatedString.Range<SpanStyle>>

// Inside edits, returned TrackedRange
fun TextFieldBuffer.getSpanStyles(range: TextRange): List<TrackedRange<SpanStyle>>
"फ़ॉर्मैट किए गए और रिच टेक्स्ट में बदलाव करना" में 20:03 पर Kotlin स्निपेट
var TrackedRange<*>.textRange: TextRange

var TrackedRange<*>.expandPolicy: ExpandPolicy

var TrackedRange<SpanStyle>.spanStyle: SpanStyle

var TrackedRange<ParagraphStyle>.paragraphStyle: ParagraphStyle

val TrackedRange<*>.isValid: Boolean

fun removeStyle(trackedRange: TrackedRange<*>): Boolean
"Building a WYSIWYG editor" में 21:01 पर Kotlin स्निपेट
fun TextFieldBuffer.toggleBoldSelection() {
    if (selection.collapsed) return
    val boldSpans = getSpanStyles(selection)

    if (!isFullyBold(boldSpans, selection)) {
        addStyle(
            SpanStyle(fontWeight = FontWeight.Bold),
            selection,
            ExpandPolicy.AtEnd
        )
    } else {
        unboldSelection(boldSpans.first())
    }
}
"Building a WYSIWYG editor" में 21:26 पर Kotlin स्निपेट
fun TextFieldBuffer.unboldSelection(
    boldSpan: TrackedRange<SpanStyle>
) {
    val hasBoldBefore = boldSpan.start < selection.min
    val hasBoldAfter = boldSpan.end > selection.max

    when {
        hasBoldBefore ->
            boldSpan.range = TextRange(range.start, selection.min)
        hasBoldAfter ->
            boldSpan.range = TextRange(selection.max, range.end)
        hasBoldBefore && hasBoldAfter -> {
            val oldEnd = boldSpan.end
            boldSpan.range = TextRange(boldSpan.start, selection.min)
            addStyle(
                boldSpan.spanStyle,
                start = selection.max,
                end = oldEnd
            )
        }
        else -> removeStyle(range)
    }
}
"Building a WYSIWYG editor" में 22:27 पर Kotlin स्निपेट
object MarkdownBoldTransformation : InputTransformation {
    val regex = Regex("""**(.*?)**""")

    override fun TextFieldBuffer.transformInput() {
        val matches = regex.findAll(asCharSequence()).toList()

        for (match in matches) {
            val range = match.range
            val content = match.groupValues[1]

            replace(range.first, range.last + 1, content)
            addStyle(
                SpanStyle(fontWeight = FontWeight.Bold),
                start = range.first,
                end = range.first + content.length,
                policy = ExpandPolicy.atEnd
            )
        }
    }
}
"WYSIWYG एडिटर बनाना" में 23:42 पर Kotlin स्निपेट
class SearchHighlightTransformation(
    val query: MutableState<String>
) : OutputTransformation {
    override fun TextFieldBuffer.transformOutput() {
        val search = query.value
        if (search.isEmpty()) return
        val text = asCharSequence().toString()
        var index = text.indexOf(search)

        while (index != -1) {
            addStyle(
                SpanStyle(
                    background = Color.Yellow,
                    color = Color.Black
                ),
                start = index,
                end = index + search.length
            )
            index = text.indexOf(search, index + search.length)
        }
    }
}