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

इस पेज पर, वैल्यू के आधार पर तय होने वाले 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 को कॉल किया जाता है.
    • TextFieldBuffer के ज़रिए InputTransformation के सुझाए गए बदलाव तुरंत लागू हो जाते हैं. इससे सॉफ़्टवेयर कीबोर्ड और 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 कंपोज़ेबल में, उन TextFieldState ऑब्जेक्ट को TextFields में पास करें.

मानकों के मुताबिक तरीका

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

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(passwordState) {
            snapshotFlow { passwordState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

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