Questa pagina fornisce esempi di come eseguire la migrazione delle TextField
basate sul valore alle TextField
basate sullo stato. Consulta la pagina Configurare i campi di testo per
informazioni sulle differenze tra i TextField
basati su valori e stati.
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 = true
conlineLimits = TextFieldLineLimits.SingleLine
.
Filtro per 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 ciclo di callback del valore con
rememberTextFieldState()
. - Reimplementa la logica di filtraggio in
onValueChange
utilizzandoInputTransformation
. - Utilizza
TextFieldBuffer
dall'ambito del destinatario diInputTransformation
per aggiornarestate
.InputTransformation
viene chiamato subito dopo il rilevamento dell'input utente.- Le modifiche proposte da
InputTransformation
tramiteTextFieldBuffer
vengono applicate immediatamente, evitando un problema di sincronizzazione tra la tastiera software eTextField
.
Formattatore di carte di credito TextField
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
onValueChange
con unInputTransformation
per impostare la lunghezza massima dell'input.- Consulta la sezione Filtrare i dati di
onValueChange
.
- Consulta la sezione Filtrare i dati di
- Sostituisci
VisualTransformation
conOutputTransformation
per aggiungere i trattini.- Con
VisualTransformation
, sei responsabile sia della creazione del nuovo testo con i trattini sia del calcolo della mappatura degli indici tra il testo visivo e lo stato di backup. OutputTransformation
si occupa automaticamente della mappatura dell'offset. Devi solo aggiungere i trattini nei punti corretti utilizzandoTextFieldBuffer
dall'ambito del destinatario diOutputTransformation.transformOutput
.
- Con
Aggiornamento dello 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 ciclo di callback del valore con
rememberTextFieldState()
. - Modifica l'assegnazione del valore con
TextFieldState.setTextAndPlaceCursorAtEnd
.
Aggiornamento dello 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 mettere il testo in grassetto intorno al cursore o alla selezione corrente. Inoltre, mantiene la posizione della selezione dopo le modifiche.
- Sostituisci il ciclo di callback del valore con
rememberTextFieldState()
. - Sostituisci
maxLines = 10
conlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Modifica la logica di calcolo di un nuovo
TextFieldValue
con una chiamataTextFieldState.edit
.- Un nuovo
TextFieldValue
viene generato unendo il testo esistente in base alla selezione corrente e inserendo le decorazioni Markdown nel mezzo. - Inoltre, la selezione viene modificata in base ai nuovi indici del testo.
TextFieldState.edit
ha un modo più naturale di modificare lo stato attuale con l'utilizzo diTextFieldBuffer
.- La selezione definisce in modo esplicito dove inserire le decorazioni.
- Poi, modifica la selezione in modo simile all'approccio
onValueChange
.
- Un nuovo
Architettura ViewModel StateFlow
Molte applicazioni seguono le linee guida per lo sviluppo di app moderne, che
promuovono l'utilizzo di un StateFlow
per definire lo stato dell'interfaccia utente 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 di solito è 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 a TextFields
che utilizzano il paradigma di sollevamento dello stato value,
onValueChange
. Tuttavia, questo approccio presenta
svantaggi imprevedibili per l'inserimento 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 di TextFieldState
non è direttamente compatibile
con lo stato dell'interfaccia utente ViewModel supportata 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.
Un consiglio comune è quello di evitare di inserire dipendenze dell'interfaccia utente in un ViewModel
.
Sebbene questa sia generalmente una buona pratica, a volte può essere interpretata in modo errato.
Ciò vale in particolare per le dipendenze di Compose che sono puramente strutture di dati e non contengono elementi 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
. Pertanto,ti invitiamo a
rivalutare il modo in cui applichi il principio "nessuna dipendenza dall'interfaccia utente in ViewModel" nel
tuo codice. Mantenere un riferimento a un TextFieldState
all'interno del tuo ViewModel
non è
una pratica intrinsecamente negativa.
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 una coppia di valoriTextFieldState
. - Passa questi oggetti
TextFieldState
aTextFields
nel composableLoginForm
.
Approccio conforme
Questi tipi di modifiche architetturali non sono sempre facili. Potresti non avere la
libertà di apportare queste modifiche o 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
ViewModel
eUiState
. - Anziché sollevare lo stato direttamente in
ViewModel
e renderlo la fonte di verità perTextFields
, trasformaViewModel
in un semplice contenitore di dati.- Per farlo, osserva le modifiche apportate a ogni
TextFieldState.text
raccogliendo unsnapshotFlow
in unLaunchedEffect
.
- Per farlo, osserva le modifiche apportate a ogni
- Il tuo
ViewModel
continuerà a mostrare i valori più recenti dell'interfaccia utente, ma il suouiState: StateFlow<UiState>
non genererà iTextField
. - Qualsiasi altra logica di persistenza implementata nel tuo
ViewModel
può rimanere invariata.