Migrar para campos de texto com base no estado

Esta página fornece exemplos de como migrar TextFields com base em valor para TextFields com base em estado. Consulte a página Configurar campos de texto para informações sobre as diferenças entre TextFields com base no valor e no estado.

Uso básico

Com base no valor

@Composable
fun OldSimpleTextField() {
    var state by rememberSaveable { mutableStateOf("") }
    TextField(
        value = state,
        onValueChange = { state = it },
        singleLine = true,
    )
}

Com base no estado

@Composable
fun NewSimpleTextField() {
    TextField(
        state = rememberTextFieldState(),
        lineLimits = TextFieldLineLimits.SingleLine
    )
}

  • Substitua value, onValueChange e remember { mutableStateOf("") } por rememberTextFieldState().
  • singleLine = true foi substituída por lineLimits = TextFieldLineLimits.SingleLine.

Filtragem por onValueChange

Com base no valor

@Composable
fun OldNoLeadingZeroes() {
    var input by rememberSaveable { mutableStateOf("") }
    TextField(
        value = input,
        onValueChange = { newText ->
            input = newText.trimStart { it == '0' }
        }
    )
}

Com base no estado

@Preview
@Composable
fun NewNoLeadingZeros() {
    TextField(
        state = rememberTextFieldState(),
        inputTransformation = InputTransformation {
            while (length > 0 && charAt(0) == '0') delete(0, 1)
        }
    )
}

  • Substitua o loop de callback de valor por rememberTextFieldState().
  • Reimplemente a lógica de filtragem em onValueChange usando InputTransformation.
  • Use TextFieldBuffer do escopo do receptor de InputTransformation para atualizar o state.
    • InputTransformation é chamado exatamente depois que a entrada do usuário é detectada.
    • As mudanças propostas por InputTransformation em TextFieldBuffer são aplicadas imediatamente, evitando um problema de sincronização entre o teclado do software e TextField.

Formatador de cartão de crédito TextField

Com base no valor

@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
                    }
                }
            )
        }
    )
}

Com base no estado

@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, "-")
        },
    )
}

  • Substitua a filtragem em onValueChange por um InputTransformation para definir o comprimento máximo da entrada.
  • Substitua VisualTransformation por OutputTransformation para adicionar traços.
    • Com VisualTransformation, você é responsável por criar o novo texto com os traços e também calcular como os índices são mapeados entre o texto visual e o estado de suporte.
    • O OutputTransformation cuida do mapeamento de deslocamento automaticamente. Basta adicionar os traços nos lugares corretos usando o TextFieldBuffer do escopo do receptor de OutputTransformation.transformOutput.

Como atualizar o estado (simples)

Com base no valor

@Composable
fun OldTextFieldStateUpdate(userRepository: UserRepository) {
    var username by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        username = userRepository.fetchUsername()
    }
    TextField(
        value = username,
        onValueChange = { username = it }
    )
}

Com base no estado

@Composable
fun NewTextFieldStateUpdate(userRepository: UserRepository) {
    val usernameState = rememberTextFieldState()
    LaunchedEffect(Unit) {
        usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername())
    }
    TextField(state = usernameState)
}

  • Substitua o loop de callback de valor por rememberTextFieldState().
  • Mude a atribuição de valor com TextFieldState.setTextAndPlaceCursorAtEnd.

Como atualizar o estado (complexo)

Com base no valor

@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
    )
}

Com base no estado

@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)
    )
}

Nesse caso de uso, um botão adiciona as decorações de Markdown para deixar o texto em negrito em torno do cursor ou da seleção atual. Ele também mantém a posição de seleção após as mudanças.

  • Substitua o loop de callback de valor por rememberTextFieldState().
  • maxLines = 10 foi substituída por lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Mude a lógica de cálculo de um novo TextFieldValue com uma chamada TextFieldState.edit.
    • Um novo TextFieldValue é gerado juntando o texto atual com base na seleção atual e inserindo as decorações do Markdown entre eles.
    • Além disso, a seleção é ajustada de acordo com os novos índices do texto.
    • O TextFieldState.edit tem uma maneira mais natural de editar o estado atual com o uso de TextFieldBuffer.
    • A seleção define explicitamente onde inserir as decorações.
    • Em seguida, ajuste a seleção, de forma semelhante à abordagem onValueChange.

Arquitetura StateFlow do ViewModel

Muitos aplicativos seguem as Diretrizes de desenvolvimento de apps modernos, que promovem o uso de um StateFlow para definir o estado da interface de uma tela ou um componente por meio de uma única classe imutável que carrega todas as informações.

Nesses tipos de aplicativos, um formulário como uma tela de login com entrada de texto geralmente é projetado da seguinte maneira:

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()
        )
    }
}

Esse design se encaixa perfeitamente com o TextFields que usa o paradigma de elevação de estado value, onValueChange. No entanto, há desvantagens imprevisíveis dessa abordagem quando se trata de entrada de texto. Os problemas de sincronização profunda com essa abordagem são explicados em detalhes na postagem do blog Gerenciamento de estado eficaz para TextField no Compose.

O problema é que o novo design da TextFieldState não é diretamente compatível com o estado da interface do ViewModel com suporte a StateFlow. Pode parecer estranho substituir username: String e password: String por username: TextFieldState e password: TextFieldState, já que TextFieldState é uma estrutura de dados inherentemente mutável.

Uma recomendação comum é evitar colocar dependências de interface em um ViewModel. Embora essa seja geralmente uma boa prática, às vezes ela pode ser mal interpretada. Isso é especialmente verdadeiro para dependências do Compose que são estruturas de dados puramente e não carregam elementos da interface, como TextFieldState.

Classes como MutableState ou TextFieldState são detentores de estado simples que são compatíveis com o sistema de estado de snapshot do Compose. Elas não são diferentes de dependências como StateFlow ou RxJava. Portanto,recomendamos reavaliar como você aplica o princípio "sem dependências de interface no ViewModel" no código. Manter uma referência a um TextFieldState no ViewModel não é uma prática ruim.

Recomendamos que você extraia valores como username ou password de UiState e mantenha uma referência separada para eles no 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)
    }
}

  • Substitua MutableStateFlow<UiState> por alguns valores TextFieldState.
  • Transmita esses objetos TextFieldState para TextFields no elemento combinável LoginForm.

Abordagem de conformidade

Esses tipos de mudanças de arquitetura nem sempre são fáceis. Talvez você não tenha liberdade para fazer essas mudanças, ou o investimento de tempo pode ser maior do que os benefícios de usar os novos TextFields. Nesse caso, ainda é possível usar campos de texto com base no estado com uma pequena modificação.

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)
    }
}

  • Mantenha as classes ViewModel e UiState iguais.
  • Em vez de elevar o estado diretamente para ViewModel e torná-lo a fonte da verdade para TextFields, transforme ViewModel em um simples detenha de dados.
    • Para fazer isso, observe as mudanças em cada TextFieldState.text coletando uma snapshotFlow em uma LaunchedEffect.
  • O ViewModel ainda terá os valores mais recentes da interface, mas o uiState: StateFlow<UiState> não vai gerar as TextFields.
  • Qualquer outra lógica de persistência implementada no ViewModel pode permanecer a mesma.