Esta página fornece exemplos de como migrar TextField
s com base em valor para
TextField
s com base em estado. Consulte a página Configurar campos de texto para
informações sobre as diferenças entre TextField
s com base no valor e no estado.
Uso básico
Com base no valor
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Com base no estado
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Substitua
value, onValueChange
eremember { mutableStateOf("")
} porrememberTextFieldState()
. singleLine = true
foi substituída porlineLimits = TextFieldLineLimits.SingleLine
.
Filtragem por onValueChange
Com base no valor
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Com base no estado
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Substitua o loop de callback de valor por
rememberTextFieldState()
. - Reimplemente a lógica de filtragem em
onValueChange
usandoInputTransformation
. - Use
TextFieldBuffer
do escopo do receptor deInputTransformation
para atualizar ostate
.InputTransformation
é chamado exatamente depois que a entrada do usuário é detectada.- As mudanças propostas por
InputTransformation
emTextFieldBuffer
são aplicadas imediatamente, evitando um problema de sincronização entre o teclado do software eTextField
.
Formatador de cartão de crédito TextField
Com base no valor
@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 } } ) } ) }
Com base no estado
@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, "-") }, ) }
- Substitua a filtragem em
onValueChange
por umInputTransformation
para definir o comprimento máximo da entrada.- Consulte a seção Filtragem por
onValueChange
.
- Consulte a seção Filtragem por
- Substitua
VisualTransformation
porOutputTransformation
para adicionar traços.- Com
VisualTransformation
, você é responsável por criar o novo texto com os traços e também calcular como os índices são mapeados entre o texto visual e o estado de suporte. - O
OutputTransformation
cuida do mapeamento de deslocamento automaticamente. Basta adicionar os traços nos lugares corretos usando oTextFieldBuffer
do escopo do receptor deOutputTransformation.transformOutput
.
- Com
Como atualizar o estado (simples)
Com base no valor
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Com base no estado
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Substitua o loop de callback de valor por
rememberTextFieldState()
. - Mude a atribuição de valor com
TextFieldState.setTextAndPlaceCursorAtEnd
.
Como atualizar o estado (complexo)
Com base no valor
@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 ) }
Com base no estado
@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) ) }
Nesse caso de uso, um botão adiciona as decorações de Markdown para deixar o texto em negrito em torno do cursor ou da seleção atual. Ele também mantém a posição de seleção após as mudanças.
- Substitua o loop de callback de valor por
rememberTextFieldState()
. maxLines = 10
foi substituída porlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
.- Mude a lógica de cálculo de um novo
TextFieldValue
com uma chamadaTextFieldState.edit
.- Um novo
TextFieldValue
é gerado juntando o texto atual com base na seleção atual e inserindo as decorações do Markdown entre eles. - Além disso, a seleção é ajustada de acordo com os novos índices do texto.
- O
TextFieldState.edit
tem uma maneira mais natural de editar o estado atual com o uso deTextFieldBuffer
. - A seleção define explicitamente onde inserir as decorações.
- Em seguida, ajuste a seleção, de forma semelhante à abordagem
onValueChange
.
- Um novo
Arquitetura StateFlow
do ViewModel
Muitos aplicativos seguem as Diretrizes de desenvolvimento de apps modernos, que
promovem o uso de um StateFlow
para definir o estado da interface de uma tela ou um componente
por meio de uma única classe imutável que carrega todas as informações.
Nesses tipos de aplicativos, um formulário como uma tela de login com entrada de texto geralmente é projetado da seguinte maneira:
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() ) } }
Esse design se encaixa perfeitamente com o TextFields
que usa o paradigma de elevação de estado value,
onValueChange
. No entanto, há desvantagens
imprevisíveis dessa abordagem quando se trata de entrada de texto. Os problemas de sincronização
profunda com essa abordagem são explicados em detalhes na postagem do blog Gerenciamento de estado
eficaz para TextField no Compose.
O problema é que o novo design da TextFieldState
não é diretamente compatível
com o estado da interface do ViewModel com suporte a StateFlow
. Pode parecer estranho substituir
username: String
e password: String
por username: TextFieldState
e
password: TextFieldState
, já que TextFieldState
é uma estrutura de dados
inherentemente mutável.
Uma recomendação comum é evitar colocar dependências de interface em um ViewModel
.
Embora essa seja geralmente uma boa prática, às vezes ela pode ser mal interpretada.
Isso é especialmente verdadeiro para dependências do Compose que são estruturas
de dados puramente e não carregam elementos da interface, como TextFieldState
.
Classes como MutableState
ou TextFieldState
são detentores de estado simples que
são compatíveis com o sistema de estado de snapshot do Compose. Elas não são diferentes de
dependências como StateFlow
ou RxJava
. Portanto,recomendamos
reavaliar como você aplica o princípio "sem dependências de interface no ViewModel" no
código. Manter uma referência a um TextFieldState
no ViewModel
não
é uma prática ruim.
Abordagem simples recomendada
Recomendamos que você extraia valores como username
ou password
de UiState
e mantenha uma referência separada para eles no 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) } }
- Substitua
MutableStateFlow<UiState>
por alguns valoresTextFieldState
. - Transmita esses objetos
TextFieldState
paraTextFields
no elemento combinávelLoginForm
.
Abordagem de conformidade
Esses tipos de mudanças de arquitetura nem sempre são fáceis. Talvez você não tenha
liberdade para fazer essas mudanças, ou o investimento de tempo pode ser maior do que os
benefícios de usar os novos TextField
s. Nesse caso, ainda é possível usar
campos de texto com base no estado com uma pequena modificação.
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) } }
- Mantenha as classes
ViewModel
eUiState
iguais. - Em vez de elevar o estado diretamente para
ViewModel
e torná-lo a fonte da verdade paraTextFields
, transformeViewModel
em um simples detenha de dados.- Para fazer isso, observe as mudanças em cada
TextFieldState.text
coletando umasnapshotFlow
em umaLaunchedEffect
.
- Para fazer isso, observe as mudanças em cada
- O
ViewModel
ainda terá os valores mais recentes da interface, mas ouiState: StateFlow<UiState>
não vai gerar asTextField
s. - Qualquer outra lógica de persistência implementada no
ViewModel
pode permanecer a mesma.