Cette page fournit des exemples de migration de TextField
basées sur la valeur vers des TextField
basées sur l'état. Pour en savoir plus sur les différences entre les TextField
basées sur la valeur et 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é sur l'état
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Remplacez
value, onValueChange
etremember { mutableStateOf("")
parrememberTextFieldState()
. - Remplacement de
singleLine = true
parlineLimits = TextFieldLineLimits.SingleLine
.
Filtrage 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é 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 deInputTransformation
pour mettre à jourstate
.InputTransformation
est appelé exactement après la détection de la saisie utilisateur.- Les modifications proposées par
InputTransformation
viaTextFieldBuffer
sont appliquées immédiatement, ce qui évite tout problème de synchronisation entre le clavier logiciel 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
onValueChange
par unInputTransformation
pour définir la longueur maximale de l'entrée.- Consultez la section Filtrer à l'aide de
onValueChange
.
- Consultez la section Filtrer à l'aide de
- Remplacez
VisualTransformation
parOutputTransformation
pour 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 la sauvegarde. OutputTransformation
gère automatiquement le mappage des décalages. Il vous suffit d'ajouter les tirets aux endroits appropriés à l'aide deTextFieldBuffer
à partir du champ d'application du récepteurOutputTransformation.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
.
Modifier 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()
. - Remplacement de
maxLines = 10
parlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Modifiez la logique de calcul d'un nouveau
TextFieldValue
avec un appelTextFieldState.edit
.- Un nouvel élément
TextFieldValue
est généré en combinant 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.edit
permet de modifier l'état actuel de manière plus naturelle à l'aide deTextFieldBuffer
.- La sélection définit explicitement l'emplacement où insérer les décorations.
- Ajustez ensuite la sélection, comme pour l'approche
onValueChange
.
- Un nouvel élément
Architecture StateFlow
du ViewModel
De nombreuses applications suivent les Consignes de développement d'applications modernes, qui encouragent 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 qui contient 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 convient parfaitement aux TextFields
qui utilisent le paradigme d'élevation d'état value,
onValueChange
. Cependant, cette approche présente des inconvénients imprévisibles en termes de saisie de texte. Les problèmes de synchronisation profonde liés à cette approche sont expliqués en détail dans l'article de blog Gérer efficacement l'état de TextField dans Compose.
Le problème est que la nouvelle conception de 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'interface utilisateur dans un ViewModel
.
Bien que cette pratique soit généralement recommandée, 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
.
Des classes telles que MutableState
ou TextFieldState
sont des conteneurs d'état simples qui sont pris en charge 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'UI dans ViewModel" dans votre code. Conserver une référence à un TextFieldState
dans votre ViewModel
n'est pas une pratique intrinsèquement mauvaise.
Approche simple recommandée
Nous vous recommandons d'extraire des valeurs telles que username
ou password
à partir de UiState
et de conserver une référence distincte pour elles 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
àTextFields
dans le composableLoginForm
.
Approche conforme
Ces types de changements d'architecture ne sont pas toujours faciles. Vous n'êtes peut-être pas libre d'apporter ces modifications, ou le temps investi pourrait être supérieur aux avantages de l'utilisation des nouvelles TextField
. Dans ce cas, vous pouvez toujours utiliser des champs de texte basés sur l'état avec un petit ajustement.
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) } }
- Laissez vos classes
ViewModel
etUiState
inchangées. - Au lieu de hisser l'état directement dans
ViewModel
et de le faire passer pour la source de vérité deTextFields
, transformezViewModel
en simple conteneur de données.- Pour ce faire, observez les modifications apportées à chaque
TextFieldState.text
en collectant unsnapshotFlow
dans unLaunchedEffect
.
- Pour ce faire, observez les modifications apportées à chaque
- Votre
ViewModel
conservera 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
ViewModel
peut rester inchangée.