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

इस पेज पर, वैल्यू के आधार पर तय किए गए 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 में फ़िल्टर करने के लॉजिक को फिर से लागू करें.
  • InputTransformation के रिसीवर स्कोप से TextFieldBuffer का इस्तेमाल करके, state को अपडेट करें.
    • 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()
        )
    }
}

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

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