Eseguire la migrazione a campi di testo basati sullo stato

Questa pagina fornisce esempi di come eseguire la migrazione di TextField basati sul valore a TextField basati sugli stati. Consulta la pagina Configurare i campi di testo per informazioni sulle differenze tra i valori e gli TextField basati sullo stato.

Utilizzo di base

Basato sul valore

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

In base allo stato

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

  • Sostituisci value, onValueChange e remember { mutableStateOf("") } con rememberTextFieldState().
  • Sostituisci singleLine = true con lineLimits = TextFieldLineLimits.SingleLine.

Filtro tramite onValueChange

Basato sul valore

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

In base allo stato

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

  • Sostituisci il loop di callback del valore con rememberTextFieldState().
  • Reimplementa la logica di filtro in onValueChange utilizzando InputTransformation.
  • Utilizza TextFieldBuffer dall'ambito del ricevitore di InputTransformation per aggiornare il state.
    • InputTransformation viene chiamato esattamente dopo il rilevamento dell'input dell'utente.
    • Le modifiche proposte da InputTransformation a TextFieldBuffer vengono applicate immediatamente, evitando un problema di sincronizzazione tra la tastiera software e TextField.

Formattatore di carte di credito TextField

Basato sul valore

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

In base allo stato

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

  • Sostituisci il filtro in onValueChange con un InputTransformation per impostare la lunghezza massima dell'input.
  • Sostituisci VisualTransformation con OutputTransformation per aggiungere i trattini.
    • Con VisualTransformation, è tua responsabilità creare il nuovo testo con i trattini e calcolare il modo in cui gli indici vengono mappati tra il testo visivo e lo stato di supporto.
    • OutputTransformation si occupa automaticamente della mappatura dell'offset. Devi solo aggiungere i trattini nei punti corretti utilizzando TextFieldBuffer dall'ambito del ricevitore di OutputTransformation.transformOutput.

Aggiornamento dello stato (semplice)

Basato sul valore

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

In base allo stato

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

  • Sostituisci il loop di callback del valore con rememberTextFieldState().
  • Modifica l'assegnazione del valore con TextFieldState.setTextAndPlaceCursorAtEnd.

Aggiornamento dello stato (complesso)

Basato sul valore

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

In base allo stato

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

In questo caso d'uso, un pulsante aggiunge le decorazioni Markdown per mettere in grassetto il testo intorno al cursore o alla selezione corrente. Mantiene inoltre la posizione della selezione dopo le modifiche.

  • Sostituisci il loop di callback del valore con rememberTextFieldState().
  • Sostituisci maxLines = 10 con lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Modifica la logica di calcolo di un nuovo TextFieldValue con una chiamata TextFieldState.edit.
    • Un nuovo TextFieldValue viene generato unendo il testo esistente in base alla selezione corrente e inserendo le decorazioni Markdown.
    • Inoltre, la selezione viene regolata in base ai nuovi indici del testo.
    • TextFieldState.edit ha un modo più naturale per modificare lo stato corrente con l'uso di TextFieldBuffer.
    • La selezione definisce esplicitamente dove inserire le decorazioni.
    • Poi, modifica la selezione, in modo simile all'approccio onValueChange.

Architettura StateFlow ViewModel

Molte applicazioni rispettano le linee guida per lo sviluppo di app moderne, che promuovono l'utilizzo di un StateFlow per definire lo stato dell'interfaccia utente di una schermata o di un componente tramite un'unica classe immutabile che contiene tutte le informazioni.

In questi tipi di applicazioni, un modulo come una schermata di accesso con input di testo viene solitamente progettato come segue:

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

Questo design si adatta perfettamente ai TextFields che utilizzano il paradigma di sollevamento dello stato value, onValueChange. Tuttavia, questo approccio presenta aspetti negativi imprevedibili per quanto riguarda l'inserimento di testo. I problemi di sincronizzazione approfondita con questo approccio sono spiegati in dettaglio nel post del blog Gestione efficace dello stato per TextField in Compose.

Il problema è che il nuovo design di TextFieldState non è direttamente compatibile con lo stato dell'interfaccia utente di ViewModel supportato da StateFlow. Potrebbe sembrare strano sostituire username: String e password: String con username: TextFieldState e password: TextFieldState, poiché TextFieldState è una struttura di dati intrinsecamente mutabile.

Un consiglio comune è evitare di inserire dipendenze dell'interfaccia utente in un ViewModel. Sebbene in genere sia una buona pratica, a volte può essere interpretata erroneamente. Questo vale in particolare per le dipendenze di Compose che sono strutture puramente di dati e non contengono elementi dell'interfaccia utente, come TextFieldState.

Classi come MutableState o TextFieldState sono semplici contenitori di stato supportati dal sistema di stato Snapshot di Compose. Non sono diverse dalle dipendenze come StateFlow o RxJava. Pertanto,ti invitiamo a rivalutare il modo in cui applichi il principio "Nessuna dipendenza dall'interfaccia utente in ViewModel" nel tuo codice. Mantenere un riferimento a un TextFieldState all'interno di ViewModel non è necessariamente una cattiva pratica.

Ti consigliamo di estrarre valori come username o password da UiState e di mantenere un riferimento separato per questi valori in 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)
    }
}

  • Sostituisci MutableStateFlow<UiState> con un paio di valori TextFieldState.
  • Passa gli oggetti TextFieldState a TextFields nel composable LoginForm.

Approccio conforme

Questi tipi di modifiche dell'architettura non sono sempre facili. Potresti non avere la libertà di apportare queste modifiche oppure l'investimento in termini di tempo potrebbe superare i vantaggi dell'utilizzo dei nuovi TextField. In questo caso, puoi comunque utilizzare i campi di testo basati sugli stati con una piccola modifica.

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

  • Mantieni invariati i corsi ViewModel e UiState.
  • Invece di sollevare lo stato direttamente in ViewModel e renderlo la verità per TextFields, trasforma ViewModel in un semplice contenitore di dati.
    • Per farlo, osserva le modifiche apportate a ogni TextFieldState.text raccogliendo un snapshotFlow in un LaunchedEffect.
  • Il tuo ViewModel avrà comunque i valori più recenti dell'interfaccia utente, ma il suo uiState: StateFlow<UiState> non gestirà i TextField.
  • Qualsiasi altra logica di persistenza implementata in ViewModel può rimanere invariata.