Zu statusbasierten Textfeldern migrieren

Auf dieser Seite finden Sie Beispiele dafür, wie Sie wertbasierte TextFields zu statusbasierten TextFields migrieren können. Auf der Seite Textfelder konfigurieren finden Sie Informationen zu den Unterschieden zwischen wert- und zustandsbasierten TextFields.

Grundlegende Nutzung

Wertbezogen

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

Statusbasiert

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

  • Ersetzen Sie value, onValueChange und remember { mutableStateOf("") } durch rememberTextFieldState().
  • Ersetzen Sie singleLine = true durch lineLimits = TextFieldLineLimits.SingleLine.

Filtern nach onValueChange

Wertbezogen

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

Statusbasiert

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

  • Ersetzen Sie die Wert-Callback-Schleife durch rememberTextFieldState().
  • Implementieren Sie die Filterlogik in onValueChange noch einmal mit InputTransformation.
  • Verwenden Sie TextFieldBuffer im Empfängerbereich von InputTransformation, um die state zu aktualisieren.
    • InputTransformation wird genau nach der Erkennung der Nutzereingabe aufgerufen.
    • Änderungen, die mit InputTransformation bis TextFieldBuffer vorgeschlagen werden, werden sofort angewendet, um Synchronisierungsprobleme zwischen der Softwaretastatur und TextField zu vermeiden.

Formatierungstool für Kreditkarten TextField

Wertbezogen

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

Statusbasiert

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

  • Ersetzen Sie die Filterung in onValueChange durch ein InputTransformation, um die maximale Länge der Eingabe festzulegen.
  • Ersetzen Sie VisualTransformation durch OutputTransformation, um Bindestriche hinzuzufügen.
    • Bei VisualTransformation müssen Sie sowohl den neuen Text mit den Bindestriche erstellen als auch berechnen, wie die Indizes zwischen dem visuellen Text und dem zugrunde liegenden Status zugeordnet werden.
    • OutputTransformation übernimmt die Offsetzuordnung automatisch. Sie müssen nur die Bindestriche an den richtigen Stellen mithilfe des TextFieldBuffer aus dem Empfängerbereich von OutputTransformation.transformOutput hinzufügen.

Status aktualisieren (einfach)

Wertbezogen

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

Statusbasiert

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

  • Ersetzen Sie die Wert-Callback-Schleife durch rememberTextFieldState().
  • Ändern Sie die Wertzuweisung mit TextFieldState.setTextAndPlaceCursorAtEnd.

Status aktualisieren (komplex)

Wertbezogen

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

Statusbasiert

@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 diesem Anwendungsfall werden die Markdown-Dekorationen über eine Schaltfläche hinzugefügt, um den Text um den Cursor oder die aktuelle Auswahl herum fett zu formatieren. Außerdem bleibt die Auswahlposition nach den Änderungen erhalten.

  • Ersetzen Sie die Wert-Callback-Schleife durch rememberTextFieldState().
  • Ersetzen Sie maxLines = 10 durch lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Ändern Sie die Logik zum Berechnen einer neuen TextFieldValue mit einem TextFieldState.edit-Aufruf.
    • Ein neuer TextFieldValue wird generiert, indem der vorhandene Text basierend auf der aktuellen Auswahl zusammengefügt und die Markdown-Dekorationen dazwischen eingefügt werden.
    • Außerdem wird die Auswahl anhand neuer Indizes des Textes angepasst.
    • In TextFieldState.edit kann der aktuelle Status mithilfe von TextFieldBuffer auf natürlichere Weise bearbeitet werden.
    • Mit der Auswahl wird explizit festgelegt, wo die Verzierungen eingefügt werden sollen.
    • Passen Sie dann die Auswahl an, ähnlich wie bei onValueChange.

ViewModel StateFlow-Architektur

Viele Anwendungen folgen den Richtlinien für die moderne App-Entwicklung, in denen die Verwendung eines StateFlow empfohlen wird, um den UI-Status eines Bildschirms oder einer Komponente über eine einzelne unveränderliche Klasse zu definieren, die alle Informationen enthält.

Bei diesen Arten von Anwendungen ist ein Formular wie ein Anmeldebildschirm mit Texteingabe in der Regel so gestaltet:

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

Dieses Design passt perfekt zu den TextFields, die das value, onValueChange-Paradigma für das Heben des Zustands verwenden. Bei der Texteingabe kann es jedoch unvorhersehbare Nachteile bei diesem Ansatz geben. Die Probleme mit der Deep Synchronization bei diesem Ansatz werden im Blogpost Effective state management for TextField in Compose (Effektive Statusverwaltung für TextField in Compose) ausführlich erläutert.

Das Problem ist, dass das neue TextFieldState-Design nicht direkt mit dem StateFlow-gestützten ViewModel-UI-Status kompatibel ist. Es mag seltsam erscheinen, username: String und password: String durch username: TextFieldState und password: TextFieldState zu ersetzen, da TextFieldState eine inhärent veränderbare Datenstruktur ist.

Es wird empfohlen, UI-Abhängigkeiten nicht in ViewModel-Dateien zu platzieren. Das ist zwar in der Regel eine gute Praxis, kann aber manchmal falsch interpretiert werden. Das gilt insbesondere für Compose-Abhängigkeiten, die reine Datenstrukturen sind und keine UI-Elemente enthalten, z. B. TextFieldState.

Klassen wie MutableState oder TextFieldState sind einfache Statushalter, die vom Snapshot-Statussystem von Compose unterstützt werden. Sie unterscheiden sich nicht von Abhängigkeiten wie StateFlow oder RxJava. Wir empfehlen Ihnen daher, noch einmal zu prüfen, wie Sie das Prinzip „Keine UI-Abhängigkeiten im ViewModel“ in Ihrem Code anwenden. Es ist nicht grundsätzlich verkehrt, in einem ViewModel auf einen TextFieldState zu verweisen.

Wir empfehlen, Werte wie username oder password aus UiState zu extrahieren und eine separate Referenz für sie in ViewModel zu speichern.

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

  • Ersetzen Sie MutableStateFlow<UiState> durch mehrere TextFieldState-Werte.
  • Übergeben Sie diese TextFieldState-Objekte in der LoginForm-Komposition an TextFields.

Konformer Ansatz

Diese Art von Architekturänderungen ist nicht immer einfach. Möglicherweise haben Sie nicht die Möglichkeit, diese Änderungen vorzunehmen, oder der Zeitaufwand überwiegt die Vorteile der neuen TextFields. In diesem Fall können Sie mit einer kleinen Anpassung weiterhin zustandsbasierte Textfelder verwenden.

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

  • Lassen Sie die ViewModel- und UiState-Klassen unverändert.
  • Anstatt den Status direkt in ViewModel zu platzieren und ihn zur Wahrheitsquelle für TextFields zu machen, machen Sie ViewModel zu einem einfachen Datenhalter.
    • Beobachten Sie dazu die Änderungen an den einzelnen TextFieldState.text, indem Sie einen snapshotFlow in einem LaunchedEffect erfassen.
  • Ihre ViewModel enthält weiterhin die neuesten Werte aus der Benutzeroberfläche, aber die uiState: StateFlow<UiState> steuert die TextField nicht.
  • Andere in Ihrem ViewModel implementierte Persistenzlogik kann unverändert bleiben.