Migrer vers des champs de texte basés sur l'état

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 et remember { mutableStateOf("") par rememberTextFieldState().
  • Remplacement de singleLine = true par lineLimits = 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 de InputTransformation.
  • Utilisez TextFieldBuffer à partir du champ d'application du récepteur de InputTransformation pour mettre à jour state.
    • InputTransformation est appelé exactement après la détection de la saisie utilisateur.
    • Les modifications proposées par InputTransformation via TextFieldBuffer sont appliquées immédiatement, ce qui évite tout problème de synchronisation entre le clavier logiciel et TextField.

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 un InputTransformation pour définir la longueur maximale de l'entrée.
  • Remplacez VisualTransformation par OutputTransformation 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 de TextFieldBuffer à partir du champ d'application du récepteur OutputTransformation.transformOutput.

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 par lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Modifiez la logique de calcul d'un nouveau TextFieldValue avec un appel TextFieldState.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 de TextFieldBuffer.
    • La sélection définit explicitement l'emplacement où insérer les décorations.
    • Ajustez ensuite la sélection, comme pour l'approche onValueChange.

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.

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 valeurs TextFieldState.
  • Transmettez ces objets TextFieldState à TextFields dans le composable LoginForm.

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 et UiState inchangées.
  • Au lieu de hisser l'état directement dans ViewModel et de le faire passer pour la source de vérité de TextFields, transformez ViewModel en simple conteneur de données.
    • Pour ce faire, observez les modifications apportées à chaque TextFieldState.text en collectant un snapshotFlow dans un LaunchedEffect.
  • Votre ViewModel conservera toujours les dernières valeurs de l'UI, mais son uiState: StateFlow<UiState> ne pilotera pas les TextField.
  • Toute autre logique de persistance implémentée dans votre ViewModel peut rester inchangée.