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

Cette page fournit des exemples de migration des TextField basés sur la valeur vers des TextField basés sur l'état. Consultez la page Configurer les 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, onValueChange et remember { mutableStateOf("") } par rememberTextFieldState().
  • Remplacement de singleLine = true par lineLimits = TextFieldLineLimits.SingleLine.

Filtrer 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 de InputTransformation.
  • Utilisez TextFieldBuffer à partir du champ d'application du récepteur de InputTransformation pour mettre à jour state.
    • InputTransformation est appelé immédiatement après la détection d'une saisie utilisateur.
    • Les modifications proposées par InputTransformation via TextFieldBuffer sont appliquées immédiatement, ce qui évite un 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 index sont mappés entre le texte visuel et l'état sous-jacent.
    • OutputTransformation s'occupe automatiquement du mappage des décalages. Il vous suffit d'ajouter les tirets aux bons endroits à l'aide de TextFieldBuffer à partir du champ d'application du récepteur de 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 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é 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().
  • 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 TextFieldValue est généré 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.edit permet de modifier l'état actuel de manière plus naturelle à l'aide de TextFieldBuffer.
    • La sélection définit explicitement où insérer les décorations.
    • Ajustez ensuite la sélection, comme dans l'approche onValueChange.

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 à l'aide d'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 d'élévation 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 en profondeur avec 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'UI ViewModel soutenu 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 souvent recommandé d'éviter de placer des dépendances d'UI 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 de pures structures de données et ne comportent aucun élément d'UI, comme TextFieldState.

Les classes telles que MutableState ou TextFieldState sont des conteneurs d'état simples qui sont soutenus 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 mauvaise pratique en soi.

Nous vous recommandons d'extraire des valeurs telles que username ou password 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 une paire de valeurs TextFieldState.
  • Transmettez ces objets TextFieldState à TextFields dans le composable LoginForm.

Approche conforme

Ces types de changements architecturaux ne sont pas toujours faciles. Vous n'aurez peut-être pas la liberté d'effectuer ces modifications, ou l'investissement en temps pourrait 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(usernameState) {
            snapshotFlow { usernameState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

  • Conservez les mêmes classes ViewModel et UiState.
  • Au lieu de hisser l'état directement dans ViewModel et d'en faire la source de vérité pour TextFields, transformez ViewModel en un simple détenteur de données.
    • Pour ce faire, observez les modifications apportées à chaque TextFieldState.text en collectant un snapshotFlow dans un LaunchedEffect.
  • Votre ViewModel aura 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 la même.