Zu statusbasierten Textfeldern migrieren

Auf dieser Seite finden Sie Beispiele für die Migration von wertbasierten TextFields zu zustandsbasierten TextFields. Informationen zu den Unterschieden zwischen wert- und zustandsbasierten TextFields finden Sie auf der Seite Textfelder konfigurieren.

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 mit InputTransformation neu.
  • Verwenden Sie TextFieldBuffer aus dem Empfängerbereich von InputTransformation, um state zu aktualisieren.
    • InputTransformation wird genau dann aufgerufen, wenn eine Nutzereingabe erkannt wird.
    • Änderungen, die von InputTransformation über TextFieldBuffer vorgeschlagen werden, werden sofort angewendet, um ein Synchronisierungsproblem zwischen der Softwaretastatur und TextField zu vermeiden.

Kreditkartenformatierer 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 einen InputTransformation, um die maximale Länge der Eingabe festzulegen.
  • Ersetzen Sie VisualTransformation durch OutputTransformation, um Bindestriche hinzuzufügen.
    • Bei VisualTransformation sind Sie sowohl für das Erstellen des neuen Texts mit den Bindestrichen als auch für das Berechnen der Zuordnung der Indexe zwischen dem visuellen Text und dem zugrunde liegenden Status verantwortlich.
    • OutputTransformation übernimmt die Offset-Zuordnung automatisch. Sie müssen nur die Bindestriche an den richtigen Stellen mit dem TextFieldBuffer aus dem Empfängerbereich von OutputTransformation.transformOutput einfü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 über eine Schaltfläche die Markdown-Dekorationen hinzugefügt, um den Text um den Cursor oder die aktuelle Auswahl fett zu formatieren. Außerdem wird die Auswahlposition nach den Änderungen beibehalten.

  • Ersetzen Sie die Wert-Callback-Schleife durch rememberTextFieldState().
  • Ersetzen Sie maxLines = 10 durch lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10).
  • Ändern Sie die Logik zur Berechnung eines neuen TextFieldValue mit einem TextFieldState.edit-Aufruf.
    • Ein neuer TextFieldValue wird generiert, indem der vorhandene Text anhand der aktuellen Auswahl aufgeteilt und die Markdown-Dekorationen dazwischen eingefügt werden.
    • Außerdem wird die Auswahl an die neuen Indexe des Texts angepasst.
    • TextFieldState.edit bietet eine natürlichere Möglichkeit, den aktuellen Status mit TextFieldBuffer zu bearbeiten.
    • Die Auswahl definiert explizit, wo die Dekorationen eingefügt werden sollen.
    • Passen Sie dann die Auswahl an, ähnlich wie beim onValueChange-Ansatz.

ViewModel-StateFlow-Architektur

Viele Anwendungen folgen den Richtlinien für die moderne App-Entwicklung, die die Verwendung eines StateFlow zur Definition des UI-Status eines Bildschirms oder einer Komponente durch eine einzelne unveränderliche Klasse, die alle Informationen enthält, empfehlen.

In solchen Anwendungen wird ein Formular wie ein Anmeldebildschirm mit Texteingabe normalerweise 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 Status-Hoisting-Paradigma value, onValueChange verwenden. Dieser Ansatz hat jedoch unvorhersehbare Nachteile, wenn es um die Texteingabe geht. Die Probleme mit der tiefen Synchronisierung bei diesem Ansatz werden im Blogpost Effective state management for TextField in Compose ausführlich erläutert.

Das Problem ist, dass das neue TextFieldState-Design nicht direkt mit dem ViewModel-UI-Status kompatibel ist, der auf StateFlow basiert. Es mag seltsam erscheinen, username: String und password: String durch username: TextFieldState und password: TextFieldState zu ersetzen, da TextFieldState eine von Natur aus veränderliche Datenstruktur ist.

Eine gängige Empfehlung ist, UI-Abhängigkeiten nicht in einem ViewModel zu platzieren. Das ist zwar im Allgemeinen eine gute Vorgehensweise, 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 State Holder, die vom Snapshot-Statussystem von Compose unterstützt werden. Sie unterscheiden sich nicht von Abhängigkeiten wie StateFlow oder RxJava. Daher empfehlen wir Ihnen, noch einmal zu prüfen, wie Sie das Prinzip „Keine UI-Abhängigkeiten in ViewModel“ in Ihrem Code anwenden. Einen Verweis auf einen TextFieldState in Ihrem ViewModel zu behalten, ist nicht grundsätzlich schlecht.

Wir empfehlen, Werte wie username oder password aus UiState zu extrahieren und einen separaten Verweis darauf in der 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 einige TextFieldState-Werte.
  • Übergib diese TextFieldState-Objekte an TextFields in der zusammensetzbaren Funktion LoginForm.

Konformer Ansatz

Diese Art von Architekturänderungen ist nicht immer einfach. Möglicherweise haben Sie nicht die Freiheit, diese Änderungen vorzunehmen, oder der Zeitaufwand könnte die Vorteile der Verwendung der neuen TextFields überwiegen. In diesem Fall können Sie zustandsbasierte Textfelder mit einer kleinen Anpassung 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(passwordState) {
            snapshotFlow { passwordState.text.toString() }.collectLatest {
                loginViewModel.updatePassword(it)
            }
        }
        SecureTextField(passwordState)
    }
}

  • Behalten Sie die Klassen ViewModel und UiState bei.
  • Anstatt den Status direkt in ViewModel zu verschieben und ihn zur „Source of Truth“ für TextFields zu machen, wandeln Sie ViewModel in einen einfachen Datenhalter um.
    • Beobachten Sie dazu die Änderungen an jedem 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> wird nicht für die TextFields verwendet.
  • Alle anderen Persistenzlogiken, die in Ihrer ViewModel implementiert sind, können unverändert bleiben.