Questa pagina fornisce esempi di come eseguire la migrazione dei TextField basati sul valore ai TextField basati sullo stato. Per informazioni sulle differenze tra i TextField basati sul valore e sullo stato, consulta la pagina Configurare i campi di testo.
Utilizzo di base
Basato sul valore
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Basato sullo stato
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Sostituisci
value, onValueChange, eremember { mutableStateOf("")} conrememberTextFieldState(). - Sostituisci
singleLine = trueconlineLimits = TextFieldLineLimits.SingleLine.
Filtrare tramite onValueChange
Basato sul valore
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Basato sullo stato
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Sostituisci il loop di callback del valore con
rememberTextFieldState(). - Reimplementa la logica di filtro in
onValueChangeutilizzandoInputTransformation. - Utilizza
TextFieldBufferdall'ambito del ricevitore diInputTransformationper aggiornare lostate.InputTransformationviene chiamato esattamente dopo il rilevamento dell'input dell'utente.- Le modifiche proposte da
InputTransformationtramiteTextFieldBuffervengono applicate immediatamente, evitando un problema di sincronizzazione tra la tastiera su schermo eTextField.
TextField del formattatore della carta di credito
Basato sul valore
@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 } } ) } ) }
Basato sullo stato
@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, "-") }, ) }
- Sostituisci il filtro in
onValueChangecon unInputTransformationper impostare la lunghezza massima dell'input.- Fai riferimento alla sezione Filtrare tramite
onValueChange.
- Fai riferimento alla sezione Filtrare tramite
- Sostituisci
VisualTransformationconOutputTransformationper aggiungere i trattini.- Con
VisualTransformation, hai la responsabilità di creare il nuovo testo con i trattini e di calcolare il modo in cui gli indici vengono mappati tra il testo visivo e lo stato di backup. OutputTransformationsi occupa automaticamente della mappatura degli offset. Devi solo aggiungere i trattini nei punti corretti utilizzandoTextFieldBufferdall'ambito del ricevitore diOutputTransformation.transformOutput.
- Con
Aggiornare lo stato (semplice)
Basato sul valore
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Basato sullo stato
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Sostituisci il loop di callback del valore con
rememberTextFieldState(). - Modifica l'assegnazione del valore con
TextFieldState.setTextAndPlaceCursorAtEnd.
Aggiornare lo stato (complesso)
Basato sul valore
@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 ) }
Basato sullo stato
@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) ) }
In questo caso d'uso, un pulsante aggiunge le decorazioni Markdown per rendere il testo in grassetto intorno al cursore o alla selezione corrente. Mantiene anche la posizione della selezione dopo le modifiche.
- Sostituisci il loop di callback del valore con
rememberTextFieldState(). - Sostituisci
maxLines = 10conlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Modifica la logica di calcolo di un nuovo
TextFieldValuecon una chiamataTextFieldState.edit.- Un nuovo
TextFieldValueviene generato unendo il testo esistente in base alla selezione corrente e inserendo le decorazioni Markdown nel mezzo. - Anche la selezione viene modificata in base ai nuovi indici del testo.
TextFieldState.editha un modo più naturale di modificare lo stato attuale con l'utilizzo diTextFieldBuffer.- La selezione definisce in modo esplicito dove inserire le decorazioni.
- Quindi, modifica la selezione, in modo simile all'approccio
onValueChange.
- Un nuovo
Architettura StateFlow di ViewModel
Molte applicazioni seguono le linee guida per lo sviluppo di app moderne, che
promuovono l'utilizzo di un StateFlow per definire lo stato dell'UI di una schermata o di un componente
tramite una singola classe immutabile che contiene tutte le informazioni.
In questi tipi di applicazioni, un modulo come una schermata di accesso con input di testo viene in genere progettato come segue:
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() ) } }
Questo design si adatta perfettamente ai TextFields che utilizzano il paradigma di innalzamento dello stato value,
onValueChange. Tuttavia, questo approccio presenta svantaggi imprevedibili per l'input di testo. I problemi di sincronizzazione approfondita
con questo approccio sono spiegati in dettaglio nel post del blog Gestione efficace dello stato
per TextField in Compose.
Il problema è che il nuovo design TextFieldState non è direttamente compatibile con lo stato dell'UI di ViewModel supportato da StateFlow. Potrebbe sembrare strano sostituire
username: String e password: String con username: TextFieldState e
password: TextFieldState, poiché TextFieldState è una struttura di dati intrinsecamente modificabile.
Una raccomandazione comune è quella di evitare di inserire dipendenze dell'UI in un ViewModel.
Sebbene questa sia in genere una buona pratica, a volte può essere interpretata in modo errato.
Questo vale in particolar modo per le dipendenze di Compose che sono puramente strutture di dati e non contengono elementi dell'UI, come TextFieldState.
Classi come MutableState o TextFieldState sono semplici contenitori di stato supportati dal sistema di stato Snapshot di Compose. Non sono diversi da dipendenze come StateFlow o RxJava. Ti invitiamo quindi a rivalutare il modo in cui applichi il principio "nessuna dipendenza dell'UI in ViewModel" nel tuo codice. Mantenere un riferimento a un TextFieldState all'interno di ViewModel non è una pratica intrinsecamente errata.
Approccio semplice consigliato
Ti consigliamo di estrarre valori come username o password da UiState e di conservare un riferimento separato per questi valori in 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) } }
- Sostituisci
MutableStateFlow<UiState>con un paio di valoriTextFieldState. - Passa questi
TextFieldStateoggetti aTextFieldsnelLoginFormcomposable.
Approccio conforme
Questi tipi di modifiche architetturali non sono sempre facili. Potresti non avere la libertà di apportare queste modifiche oppure l'investimento di tempo potrebbe superare i vantaggi dell'utilizzo dei nuovi TextField. In questo caso, puoi comunque utilizzare i campi di testo basati sullo stato con una piccola modifica.
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) } }
- Mantieni invariate le classi
ViewModeleUiState. - Anziché sollevare lo stato direttamente in
ViewModele renderlo l' origine della verità perTextFields, trasformaViewModelin un semplice contenitore di dati.- Per farlo, osserva le modifiche a ogni
TextFieldState.textraccogliendo unsnapshotFlowin unLaunchedEffect.
- Per farlo, osserva le modifiche a ogni
- Il tuo
ViewModelcontinuerà a contenere gli ultimi valori dell'UI, ma il suouiState: StateFlow<UiState>non gestirà iTextFields. - Qualsiasi altra logica di persistenza implementata in
ViewModelpuò rimanere invariata.