Cette page fournit des exemples de migration de TextField basés sur des valeurs vers des TextField basés sur l'état. Consultez la page Configurer des champs de texte pour en savoir plus sur les différences entre les TextField basés sur la valeur et ceux basés sur l'état.
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é sur l'état
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Remplacez
value, onValueChangeetremember { mutableStateOf("")parrememberTextFieldState(). - Remplacez
singleLine = trueparlineLimits = TextFieldLineLimits.SingleLine.
Filtrage par 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é 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 à jourstate.InputTransformationest appelé exactement après la détection d'une 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.
Outil de mise en forme des cartes de crédit TextField
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é 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 unInputTransformationpour 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 devez créer le texte avec les tirets et calculer la façon dont les indices sont mappés entre le texte visuel et l'état de sauvegarde. OutputTransformationgère automatiquement le mappage des décalages. 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é 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 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é 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 en gras le texte autour du curseur ou de la sélection actuelle. Il conserve également la position de 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'un nouveau
TextFieldValueavec un appelTextFieldState.edit.- Un nouveau
TextFieldValueest généré en découpant 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 index du texte.
TextFieldState.editoffre un moyen plus naturel de modifier l'état actuel à l'aide deTextFieldBuffer.- La sélection définit explicitement l'emplacement où insérer les décorations.
- Ensuite, ajustez la sélection, comme pour l'approche
onValueChange.
- Un nouveau
Architecture ViewModel StateFlow
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'UI 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 saisie 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 paradigme de hissage d'état value,
onValueChange. Cependant, cette approche présente des inconvénients imprévisibles en ce qui concerne la saisie de texte. Les problèmes de synchronisation profonde liés à cette approche sont expliqués en détail dans l'article de blog Effective state management for TextField in Compose (Gérer efficacement l'état de TextField dans Compose).
Le problème est que la nouvelle conception TextFieldState n'est pas directement compatible avec l'état de l'UI ViewModel basé sur 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 intrinsèquement modifiable.
Il est généralement recommandé d'éviter de placer des dépendances d'UI 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 s'appuient sur le système d'état d'instantané 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'UI 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 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 à mettre en œuvre. Vous n'avez peut-être pas la liberté d'apporter ces modifications, ou l'investissement en temps peut l'emporter sur les 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(passwordState) { snapshotFlow { passwordState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
- Conservez les mêmes classes
ViewModeletUiState. - Au lieu de hisser 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'UI, mais sonuiState: StateFlow<UiState>ne pilotera pas lesTextField. - Toute autre logique de persistance implémentée dans votre
ViewModelpeut rester identique.