स्टेटस पर आधारित टेक्स्ट फ़ील्ड पर माइग्रेट करना

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

बुनियादी इस्तेमाल

वैल्यू के आधार पर

@Composable
fun OldSimpleTextField() {
    var state by rememberSaveable { mutableStateOf("") }
    TextField(
        value = state,
        onValueChange = { state = it },
        singleLine = true,
    )
}

राज्य के हिसाब से

@Composable
fun NewSimpleTextField() {
    TextField(
        state = rememberTextFieldState(),
        lineLimits = TextFieldLineLimits.SingleLine
    )
}

  • value, onValueChange और remember { mutableStateOf("") } को rememberTextFieldState() से बदलें.
  • singleLine = true को lineLimits = TextFieldLineLimits.SingleLine से बदलें.

onValueChange के हिसाब से फ़िल्टर करना

वैल्यू के आधार पर

@Composable
fun OldNoLeadingZeroes() {
    var input by rememberSaveable { mutableStateOf("") }
    TextField(
        value = input,
        onValueChange = { newText ->
            input = newText.trimStart { it == '0' }
        }
    )
}

राज्य के हिसाब से

@Preview
@Composable
fun NewNoLeadingZeros() {
    TextField(
        state = rememberTextFieldState(),
        inputTransformation = InputTransformation {
            while (length > 0 && charAt(0) == '0') delete(0, 1)
        }
    )
}

  • वैल्यू कॉलबैक लूप को rememberTextFieldState() से बदलें.
  • InputTransformation का इस्तेमाल करके, onValueChange में फ़िल्टर करने का लॉजिक फिर से लागू करें.
  • state को अपडेट करने के लिए, InputTransformation के रिसीवर स्कोप में मौजूद TextFieldBuffer का इस्तेमाल करें.
    • InputTransformation को उपयोगकर्ता इनपुट का पता चलने के ठीक बाद कॉल किया जाता है.
    • InputTransformation से TextFieldBuffer के ज़रिए सुझाए गए बदलाव तुरंत लागू हो जाते हैं. इससे, सॉफ़्टवेयर कीबोर्ड और TextField के बीच सिंक करने से जुड़ी समस्या से बचा जा सकता है.

क्रेडिट कार्ड फ़ॉर्मैटर TextField

वैल्यू के आधार पर

@Composable
fun OldTextFieldCreditCardFormatter() {
    var state by remember { mutableStateOf("") }
    TextField(
        value = state,
        onValueChange = { if (it.length <= 16) state = it },
        visualTransformation = VisualTransformation { text ->
            // Making XXXX-XXXX-XXXX-XXXX string.
            var out = ""
            for (i in text.indices) {
                out += text[i]
                if (i % 4 == 3 && i != 15) out += "-"
            }

            TransformedText(
                text = AnnotatedString(out),
                offsetMapping = object : OffsetMapping {
                    override fun originalToTransformed(offset: Int): Int {
                        if (offset <= 3) return offset
                        if (offset <= 7) return offset + 1
                        if (offset <= 11) return offset + 2
                        if (offset <= 16) return offset + 3
                        return 19
                    }

                    override fun transformedToOriginal(offset: Int): Int {
                        if (offset <= 4) return offset
                        if (offset <= 9) return offset - 1
                        if (offset <= 14) return offset - 2
                        if (offset <= 19) return offset - 3
                        return 16
                    }
                }
            )
        }
    )
}

राज्य के हिसाब से

@Composable
fun NewTextFieldCreditCardFormatter() {
    val state = rememberTextFieldState()
    TextField(
        state = state,
        inputTransformation = InputTransformation.maxLength(16),
        outputTransformation = OutputTransformation {
            if (length > 4) insert(4, "-")
            if (length > 9) insert(9, "-")
            if (length > 14) insert(14, "-")
        },
    )
}

  • इनपुट की ज़्यादा से ज़्यादा लंबाई सेट करने के लिए, onValueChange में फ़िल्टर करने की सुविधा को InputTransformation से बदलें.
  • डैश जोड़ने के लिए, VisualTransformation को OutputTransformation से बदलें.
    • VisualTransformation का इस्तेमाल करने पर, आपको डैश के साथ नया टेक्स्ट बनाना होगा. साथ ही, यह भी हिसाब लगाना होगा कि विज़ुअल टेक्स्ट और बैकिंग स्टेटस के बीच इंडेक्स कैसे मैप किए जाते हैं.
    • OutputTransformation, ऑफ़सेट मैपिंग को अपने-आप मैनेज करता है. आपको OutputTransformation.transformOutput के रिसीवर स्कोप से TextFieldBuffer का इस्तेमाल करके, सही जगहों पर सिर्फ़ डैश जोड़ने होंगे.

स्थिति अपडेट करना (सरल)

वैल्यू के आधार पर

@Composable
fun OldTextFieldStateUpdate(userRepository: UserRepository) {
    var username by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        username = userRepository.fetchUsername()
    }
    TextField(
        value = username,
        onValueChange = { username = it }
    )
}

राज्य के हिसाब से

@Composable
fun NewTextFieldStateUpdate(userRepository: UserRepository) {
    val usernameState = rememberTextFieldState()
    LaunchedEffect(Unit) {
        usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername())
    }
    TextField(state = usernameState)
}

  • वैल्यू कॉलबैक लूप को rememberTextFieldState() से बदलें.
  • वैल्यू असाइनमेंट को TextFieldState.setTextAndPlaceCursorAtEnd से बदलें.

स्टेटस अपडेट करना (जटिल)

वैल्यू के आधार पर

@Composable
fun OldTextFieldAddMarkdownEmphasis() {
    var markdownState by remember { mutableStateOf(TextFieldValue()) }
    Button(onClick = {
        // add ** decorations around the current selection, also preserve the selection
        markdownState = with(markdownState) {
            copy(
                text = buildString {
                    append(text.take(selection.min))
                    append("**")
                    append(text.substring(selection))
                    append("**")
                    append(text.drop(selection.max))
                },
                selection = TextRange(selection.min + 2, selection.max + 2)
            )
        }
    }) {
        Text("Bold")
    }
    TextField(
        value = markdownState,
        onValueChange = { markdownState = it },
        maxLines = 10
    )
}

राज्य के हिसाब से

@Composable
fun NewTextFieldAddMarkdownEmphasis() {
    val markdownState = rememberTextFieldState()
    LaunchedEffect(Unit) {
        // add ** decorations around the current selection
        markdownState.edit {
            insert(originalSelection.max, "**")
            insert(originalSelection.min, "**")
            selection = TextRange(originalSelection.min + 2, originalSelection.max + 2)
        }
    }
    TextField(
        state = markdownState,
        lineLimits = TextFieldLineLimits.MultiLine(1, 10)
    )
}

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

  • वैल्यू कॉलबैक लूप को rememberTextFieldState() से बदलें.
  • maxLines = 10 को lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10) से बदलें.
  • TextFieldState.edit कॉल की मदद से, नए TextFieldValue का हिसाब लगाने का लॉजिक बदलें.
    • मौजूदा टेक्स्ट को चुनकर, उसके बीच में मार्कडाउन डेकोरेशन डालकर, नया TextFieldValue जनरेट किया जाता है.
    • साथ ही, चुने गए हिस्से को टेक्स्ट के नए इंडेक्स के हिसाब से अडजस्ट किया जाता है.
    • TextFieldState.edit में TextFieldBuffer का इस्तेमाल करके, मौजूदा स्थिति में बदलाव करने का ज़्यादा आसान तरीका है.
    • चुने गए विकल्प से साफ़ तौर पर पता चलता है कि सजावट कहां डालनी है.
    • इसके बाद, onValueChange के तरीके की तरह ही, चुने गए आइटम में बदलाव करें.

ViewModel StateFlow का आर्किटेक्चर

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

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

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState>
        get() = _uiState.asStateFlow()

    fun updateUsername(username: String) = _uiState.update { it.copy(username = username) }

    fun updatePassword(password: String) = _uiState.update { it.copy(password = password) }
}

data class UiState(
    val username: String = "",
    val password: String = ""
)

@Composable
fun LoginForm(
    loginViewModel: LoginViewModel,
    modifier: Modifier = Modifier
) {
    val uiState by loginViewModel.uiState.collectAsStateWithLifecycle()
    Column(modifier) {
        TextField(
            value = uiState.username,
            onValueChange = { loginViewModel.updateUsername(it) }
        )
        TextField(
            value = uiState.password,
            onValueChange = { loginViewModel.updatePassword(it) },
            visualTransformation = PasswordVisualTransformation()
        )
    }
}

यह डिज़ाइन, value, onValueChange स्टेटस होस्टिंग पैराडाइम का इस्तेमाल करने वाले TextFields के साथ पूरी तरह से फ़िट बैठता है. हालांकि, टेक्स्ट इनपुट के मामले में, इस तरीके के कुछ नुकसान हो सकते हैं. इस तरीके से डीप सिंक करने से जुड़ी समस्याओं के बारे में ज़्यादा जानकारी, Compose में TextField के लिए बेहतर स्टेटस मैनेजमेंट ब्लॉग पोस्ट में दी गई है.

समस्या यह है कि TextFieldState का नया डिज़ाइन, StateFlow के साथ काम करने वाले ViewModel यूज़र इंटरफ़ेस (यूआई) स्टेटस के साथ सीधे तौर पर काम नहीं करता. username: String और password: String को username: TextFieldState और password: TextFieldState से बदलना अजीब लग सकता है. ऐसा इसलिए, क्योंकि TextFieldState एक ऐसा डेटा स्ट्रक्चर है जिसे आसानी से बदला जा सकता है.

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

MutableState या TextFieldState जैसी क्लास, स्टेटस के सामान्य होल्डर होती हैं. इन्हें Compose के स्नैपशॉट स्टेटस सिस्टम की मदद से मैनेज किया जाता है. ये StateFlow या RxJava जैसी डिपेंडेंसी से अलग नहीं हैं. इसलिए, हमारा सुझाव है कि आप अपने कोड में, "ViewModel में कोई यूज़र इंटरफ़ेस (यूआई) डिपेंडेंसी नहीं" सिद्धांत को लागू करने के तरीके का फिर से आकलन करें. अपने ViewModel में TextFieldState का रेफ़रंस रखना, ज़रूरी नहीं है कि गलत हो.

हमारा सुझाव है कि आप UiState से username या password जैसी वैल्यू निकालें और ViewModel में उनके लिए अलग से रेफ़रंस रखें.

class LoginViewModel : ViewModel() {
    val usernameState = TextFieldState()
    val passwordState = TextFieldState()
}

@Composable
fun LoginForm(
    loginViewModel: LoginViewModel,
    modifier: Modifier = Modifier
) {
    Column(modifier) {
        TextField(state = loginViewModel.usernameState,)
        SecureTextField(state = loginViewModel.passwordState)
    }
}

  • MutableStateFlow<UiState> को कुछ TextFieldState वैल्यू से बदलें.
  • LoginForm composable में, उन TextFieldState ऑब्जेक्ट को TextFields पर पास करें.

नियमों का पालन करने वाला तरीका

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

class LoginViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState>
        get() = _uiState.asStateFlow()

    fun updateUsername(username: String) = _uiState.update { it.copy(username = username) }

    fun updatePassword(password: String) = _uiState.update { it.copy(password = password) }
}

data class UiState(
    val username: String = "",
    val password: String = ""
)

@Composable
fun LoginForm(
    loginViewModel: LoginViewModel,
    modifier: Modifier = Modifier
) {
    val initialUiState = remember(loginViewModel) { loginViewModel.uiState.value }
    Column(modifier) {
        val usernameState = rememberTextFieldState(initialUiState.username)
        LaunchedEffect(usernameState) {
            snapshotFlow { usernameState.text.toString() }.collectLatest {
                loginViewModel.updateUsername(it)
            }
        }
        TextField(usernameState)

        val passwordState = rememberTextFieldState(initialUiState.password)
        LaunchedEffect(usernameState) {
            snapshotFlow { usernameState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

  • ViewModel और UiState क्लास को एक जैसा रखें.
  • ViewModel में सीधे स्टेटस को होस्ट करने और उसे TextFields के लिए सटीक जानकारी का सोर्स बनाने के बजाय, ViewModel को डेटा होल्डर में बदलें.
    • ऐसा करने के लिए, LaunchedEffect में snapshotFlow इकट्ठा करके, हर TextFieldState.text में हुए बदलावों को देखें.
  • आपके ViewModel में अब भी यूज़र इंटरफ़ेस (यूआई) से मिली नई वैल्यू होंगी. हालांकि, इसका uiState: StateFlow<UiState>, TextField को ड्राइव नहीं करेगा.
  • आपके ViewModel में लागू किया गया कोई भी अन्य पर्सिस्टेंस लॉजिक पहले जैसा ही रह सकता है.