Cette page fournit des exemples de migration de TextField basés sur la valeur vers des TextField basés sur l'état. Pour en savoir plus sur les différences entre les TextField basés sur la valeur et ceux basés sur l'état, consultez la page Configurer les champs de texte.
Utilisation de base
basées sur la valeur
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
basées sur l'état
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Remplacez
value, onValueChange, etremember { mutableStateOf("")} parrememberTextFieldState(). - Remplacez
singleLine = trueparlineLimits = TextFieldLineLimits.SingleLine.
Filtrer via onValueChange
basées sur la valeur
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
basées sur l'état
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Remplacez la boucle de rappel de valeur par
rememberTextFieldState(). - Réimplémentez la logique de filtrage dans
onValueChangeà l'aide deInputTransformation. - Utilisez
TextFieldBufferà partir du champ d'application du récepteur deInputTransformationpour mettre à jour lestate.InputTransformationest appelé juste après la détection de l'entrée utilisateur.- Les modifications proposées par
InputTransformationviaTextFieldBuffersont appliquées immédiatement, ce qui évite un problème de synchronisation entre le clavier virtuel etTextField.
TextField du formateur de carte de crédit
basées sur la valeur
@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 } } ) } ) }
basées sur l'état
@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, "-") }, ) }
- Remplacez le filtrage dans
onValueChangepar uneInputTransformationpour définir la longueur maximale de l'entrée.- Consultez la section Filtrer via
onValueChange.
- Consultez la section Filtrer via
- Remplacez
VisualTransformationparOutputTransformationpour ajouter des tirets.- Avec
VisualTransformation, vous êtes responsable à la fois de la création du nouveau texte avec les tirets et du calcul de la façon dont les indices sont mappés entre le texte visuel et l'état de sauvegarde. OutputTransformationgère automatiquement le mappage de décalage. Il vous suffit d'ajouter les tirets aux bons endroits à l'aide deTextFieldBufferà partir du champ d'application du récepteur deOutputTransformation.transformOutput.
- Avec
Mettre à jour l'état (simple)
basées sur la valeur
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
basées sur l'état
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Remplacez la boucle de rappel de valeur par
rememberTextFieldState(). - Modifiez l'attribution de la valeur avec
TextFieldState.setTextAndPlaceCursorAtEnd.
Mettre à jour l'état (complexe)
basées sur la valeur
@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 ) }
basées sur l'état
@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) ) }
Dans ce cas d'utilisation, un bouton ajoute les décorations Markdown pour mettre le texte en gras autour du curseur ou de la sélection actuelle. Il conserve également la position de la sélection après les modifications.
- Remplacez la boucle de rappel de valeur par
rememberTextFieldState(). - Remplacez
maxLines = 10parlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Modifiez la logique de calcul d'une nouvelle
TextFieldValueavec un appelTextFieldState.edit.- Une nouvelle
TextFieldValueest générée en épissant le texte existant en fonction de la sélection actuelle et en insérant les décorations Markdown entre les deux. - La sélection est également ajustée en fonction des nouveaux indices du texte.
TextFieldState.editpermet de modifier l'état actuel de manière plus naturelle à l'aide deTextFieldBuffer.- La sélection définit explicitement où insérer les décorations.
- Ajustez ensuite la sélection, comme avec l'approche
onValueChange.
- Une nouvelle
Architecture StateFlow de ViewModel
De nombreuses applications suivent les consignes de développement d'applications modernes, qui
préconisent l'utilisation d'un StateFlow pour définir l'état de l'interface utilisateur d'un écran ou d'un composant
via une seule classe immuable contenant toutes les informations.
Dans ces types d'applications, un formulaire tel qu'un écran de connexion avec une entrée de texte est généralement conçu comme suit :
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() ) } }
Cette conception s'adapte parfaitement aux TextFields qui utilisent le value,
onValueChange paradigme de levée d'état. Toutefois, cette approche présente des inconvénients imprévisibles en ce qui concerne l'entrée de texte. Les problèmes de synchronisation profonds liés à cette approche sont expliqués en détail dans l'article de blog Gestion efficace de l'état pour TextField dans Compose.
Le problème est que la nouvelle conception TextFieldState n'est pas directement compatible avec l'état de l'interface utilisateur ViewModel sauvegardé par StateFlow. Il peut sembler étrange de remplacer
username: String et password: String par username: TextFieldState et
password: TextFieldState, car TextFieldState est une structure de données mutable par nature.
Il est généralement recommandé d'éviter de placer des dépendances d'interface utilisateur dans un ViewModel.
Bien qu'il s'agisse généralement d'une bonne pratique, elle peut parfois être mal interprétée.
Cela est particulièrement vrai pour les dépendances Compose qui sont purement des structures de données et ne comportent aucun élément d'interface utilisateur, comme TextFieldState.
Les classes telles que MutableState ou TextFieldState sont des conteneurs d'état simples qui sont sauvegardés par le système d'état Snapshot de Compose. Elles ne sont pas différentes des dépendances telles que StateFlow ou RxJava. Par conséquent,nous vous encourageons à réévaluer la façon dont vous appliquez le principe "aucune dépendance d'interface utilisateur dans ViewModel" dans votre code. Conserver une référence à un TextFieldState dans votre ViewModel n'est pas une mauvaise pratique en soi.
Approche simple recommandée
Nous vous recommandons d'extraire des valeurs telles que username ou password de UiState, et de conserver une référence distincte pour celles-ci dans le 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) } }
- Remplacez
MutableStateFlow<UiState>par quelques valeursTextFieldState. - Transmettez ces objets
TextFieldStateàTextFieldsdans le composableLoginForm.
Approche conforme
Ces types de modifications architecturales ne sont pas toujours faciles. Vous n'avez peut-être pas la possibilité d'effectuer ces modifications, ou l'investissement en temps peut être supérieur aux avantages de l'utilisation des nouveaux TextField. Dans ce cas, vous pouvez toujours utiliser des champs de texte basés sur l'état avec une petite modification.
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) } }
- Conservez les mêmes classes
ViewModeletUiState. - Au lieu de lever l'état directement dans
ViewModelet d'en faire la source de vérité pourTextFields, transformezViewModelen un simple conteneur de données.- Pour ce faire, observez les modifications apportées à chaque
TextFieldState.texten collectant unsnapshotFlowdans unLaunchedEffect.
- Pour ce faire, observez les modifications apportées à chaque
- Votre
ViewModelcontiendra toujours les dernières valeurs de l'interface utilisateur, mais sonuiState: StateFlow<UiState>ne pilotera pas lesTextFields. - Toute autre logique de persistance implémentée dans votre
ViewModelpeut rester la même.