Nesta página, mostramos exemplos de como migrar TextFields baseados em valor para
TextFields baseados em estado. Consulte a página Configurar campos de texto para
ver informações sobre as diferenças entre TextFields baseados em valor e em 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, onValueChangeeremember { mutableStateOf("")porrememberTextFieldState(). - Substitua
singleLine = trueporlineLimits = 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(). - Implemente novamente a lógica de filtragem em
onValueChangeusandoInputTransformation. - Use
TextFieldBufferdo escopo do receptor deInputTransformationpara atualizar ostate.InputTransformationé chamado exatamente após a detecção da entrada do usuário.- As mudanças propostas por
InputTransformationusandoTextFieldBuffersão aplicadas imediatamente, evitando um problema de sincronização entre o teclado de 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
onValueChangepor umInputTransformationpara definir o tamanho máximo da entrada.- Consulte a seção Como filtrar por
onValueChange.
- Consulte a seção Como filtrar por
- Substitua
VisualTransformationporOutputTransformationpara adicionar traços.- Com
VisualTransformation, você é responsável por criar o novo texto com os traços e também por calcular como os índices são mapeados entre o texto visual e o estado de apoio. - O
OutputTransformationcuida do mapeamento de deslocamento automaticamente. Você só precisa adicionar os traços nos lugares certos usando oTextFieldBufferdo 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 do Markdown para deixar o texto em negrito ao redor 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(). - Substitua
maxLines = 10porlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Mude a lógica de cálculo de um novo
TextFieldValuecom uma chamadaTextFieldState.edit.- Um novo
TextFieldValueé gerado ao dividir o texto atual com base na seleção atual e inserir as decorações de Markdown entre elas. - Além disso, a seleção é ajustada de acordo com os novos índices do texto.
- O
TextFieldState.edittem 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 do ViewModel StateFlow
Muitos aplicativos seguem as diretrizes de desenvolvimento de apps modernos, que
promovem o uso de um StateFlow para definir o estado da IU de uma tela ou de um componente
com 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 os TextFields que usam o paradigma de elevação de estado value,
onValueChange. No entanto, há desvantagens imprevisíveis
nessa 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 (link em inglês).
O problema é que o novo design de TextFieldState não é diretamente compatível
com o estado da interface do ViewModel com suporte de StateFlow. Pode parecer estranho substituir
username: String e password: String por username: TextFieldState e
password: TextFieldState, já que TextFieldState é uma estrutura de dados
inerentemente mutável.
Uma recomendação comum é evitar colocar dependências de IU em um ViewModel.
Embora essa seja uma boa prática, às vezes ela pode ser mal interpretada.
Isso é especialmente verdadeiro para dependências do Compose que são puramente estruturas de dados
e não carregam elementos da interface, como TextFieldState.
Classes como MutableState ou TextFieldState são detentores de estado simples que
usam o sistema de estado de snapshot do Compose. Eles não são diferentes de
dependências como StateFlow ou RxJava. Portanto,recomendamos que você
reavalie como aplica o princípio "sem dependências de IU no ViewModel" no
seu código. Manter uma referência a um TextFieldState no ViewModel não é
uma prática inerentemente ruim.
Abordagem simples recomendada
Recomendamos extrair valores como username ou password de UiState
e manter 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 valores deTextFieldState. - Transmita esses objetos
TextFieldStateparaTextFieldsno elemento combinávelLoginForm.
Abordagem de conformidade
Esses tipos de mudanças arquitetônicas nem sempre são fáceis. Talvez você não tenha a
liberdade de fazer essas mudanças ou o investimento de tempo pode superar os
benefícios de usar as novas TextFields. Nesse caso, ainda é possível usar
campos de texto baseados em estado com um pequeno ajuste.
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) } }
- Mantenha as classes
ViewModeleUiStateiguais. - Em vez de elevar o estado diretamente para
ViewModele torná-lo a fonte de verdade paraTextFields, transformeViewModelem um simples detentor de dados.- Para fazer isso, observe as mudanças em cada
TextFieldState.textcoletando umsnapshotFlowem umLaunchedEffect.
- Para fazer isso, observe as mudanças em cada
- Seu
ViewModelainda terá os valores mais recentes da interface, mas ouiState: StateFlow<UiState>não vai mais controlar osTextFields. - Qualquer outra lógica de persistência implementada no
ViewModelpode permanecer igual.