En esta página, se proporcionan ejemplos de cómo puedes migrar TextField basadas en valores a
TextField basadas en estados. Consulta la página Cómo configurar campos de texto para obtener información sobre las diferencias entre los TextField basados en valores y en estados.
Uso básico
Ofertas
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Basado en el estado
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Reemplaza
value, onValueChangeyremember { mutableStateOf("")porrememberTextFieldState(). - Reemplaza
singleLine = trueporlineLimits = TextFieldLineLimits.SingleLine.
Filtrado por onValueChange
Ofertas
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Basado en el estado
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Reemplaza el bucle de devolución de llamada de valor por
rememberTextFieldState(). - Vuelve a implementar la lógica de filtrado en
onValueChangeconInputTransformation. - Usa
TextFieldBufferdel alcance del receptor deInputTransformationpara actualizarstate.- Se llama a
InputTransformationexactamente después de que se detecta la entrada del usuario. - Los cambios que propone
InputTransformationa través deTextFieldBufferse aplican de inmediato, lo que evita un problema de sincronización entre el teclado en pantalla yTextField.
- Se llama a
Formateador de tarjetas de crédito TextField
Ofertas
@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 } } ) } ) }
Basado en el 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, "-") }, ) }
- Reemplaza el filtrado en
onValueChangepor unInputTransformationpara establecer la longitud máxima de la entrada.- Consulta la sección Filtrado a través de
onValueChange.
- Consulta la sección Filtrado a través de
- Reemplaza
VisualTransformationporOutputTransformationpara agregar guiones.- Con
VisualTransformation, eres responsable de crear el texto nuevo con los guiones y de calcular cómo se asignan los índices entre el texto visual y el estado de respaldo. OutputTransformationse encarga de la asignación de compensación automáticamente. Solo debes agregar los guiones en los lugares correctos con elTextFieldBufferdel alcance del receptor deOutputTransformation.transformOutput.
- Con
Actualiza el estado (simple)
Ofertas
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Basado en el estado
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Reemplaza el bucle de devolución de llamada de valor por
rememberTextFieldState(). - Cambia la asignación de valor con
TextFieldState.setTextAndPlaceCursorAtEnd.
Actualiza el estado (complejo)
Ofertas
@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 ) }
Basado en el 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) ) }
En este caso de uso, un botón agrega las decoraciones de Markdown para que el texto alrededor del cursor o la selección actual aparezca en negrita. También mantiene la posición de la selección después de los cambios.
- Reemplaza el bucle de devolución de llamada de valor por
rememberTextFieldState(). - Reemplaza
maxLines = 10porlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Cambia la lógica de cálculo de un
TextFieldValuenuevo con una llamada aTextFieldState.edit.- Se genera un nuevo
TextFieldValueuniendo el texto existente según la selección actual y, luego, insertando las decoraciones de Markdown en el medio. - Además, la selección se ajusta según los nuevos índices del texto.
TextFieldState.edittiene una forma más natural de editar el estado actual con el uso deTextFieldBuffer.- La selección define de forma explícita dónde insertar las decoraciones.
- Luego, ajusta la selección, de manera similar al enfoque de
onValueChange.
- Se genera un nuevo
Arquitectura de ViewModel StateFlow
Muchas aplicaciones siguen los lineamientos de desarrollo de apps modernas, que promueven el uso de un StateFlow para definir el estado de la IU de una pantalla o un componente a través de una sola clase inmutable que contiene toda la información.
En estos tipos de aplicaciones, un formulario como una pantalla de acceso con entrada de texto suele diseñarse de la siguiente manera:
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() ) } }
Este diseño se adapta perfectamente a los TextFields que usan el paradigma de elevación de estado de value,
onValueChange. Sin embargo, este enfoque tiene desventajas impredecibles en lo que respecta a la entrada de texto. Los problemas de sincronización profunda con este enfoque se explican en detalle en la entrada de blog Administración eficaz del estado para TextField en Compose.
El problema es que el nuevo diseño de TextFieldState no es directamente compatible con el estado de la IU de ViewModel respaldado por StateFlow. Puede parecer extraño reemplazar username: String y password: String por username: TextFieldState y password: TextFieldState, ya que TextFieldState es una estructura de datos inherentemente mutable.
Una recomendación común es evitar colocar dependencias de la IU en un ViewModel.
Aunque, por lo general, esta es una práctica recomendada, a veces se puede malinterpretar.
Esto es especialmente cierto para las dependencias de Compose que son puramente estructuras de datos y no llevan ningún elemento de la IU, como TextFieldState.
Las clases como MutableState o TextFieldState son contenedores de estado simples que están respaldados por el sistema de estado de instantáneas de Compose. No son diferentes de las dependencias como StateFlow o RxJava. Por lo tanto,te recomendamos que vuelvas a evaluar cómo aplicas el principio de "no hay dependencias de la IU en ViewModel" en tu código. Mantener una referencia a un TextFieldState dentro de tu ViewModel no es una práctica inherentemente mala.
Enfoque simple recomendado
Te recomendamos que extraigas valores como username o password de UiState
y mantengas una referencia separada para ellos en 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) } }
- Reemplaza
MutableStateFlow<UiState>por un par de valoresTextFieldState. - Pasa esos objetos
TextFieldStateaTextFieldsen el elemento componibleLoginForm.
Enfoque de conformidad
Estos tipos de cambios de arquitectura no siempre son fáciles. Es posible que no tengas la
libertad de realizar estos cambios o que la inversión de tiempo supere los
beneficios de usar los nuevos TextField. En este caso, puedes seguir usando campos de texto basados en el estado con un pequeño 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) } }
- Mantén tus clases
ViewModelyUiStatesin cambios. - En lugar de elevar el estado directamente a
ViewModely convertirlo en la fuente de información deTextFields, convierte aViewModelen un contenedor de datos simple.- Para ello, observa los cambios en cada
TextFieldState.textrecopilando unsnapshotFlowen unLaunchedEffect.
- Para ello, observa los cambios en cada
- Tu
ViewModelseguirá teniendo los valores más recientes de la IU, pero suuiState: StateFlow<UiState>no controlará losTextField. - Cualquier otra lógica de persistencia implementada en tu
ViewModelpuede permanecer igual.