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 statusbasierten 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 direkt nach der Erkennung einer Nutzereingabe aufgerufen.
    • Änderungen, die von InputTransformation über TextFieldBuffer vorgeschlagen werden, werden sofort angewendet. So wird ein Synchronisierungsproblem zwischen der Softwaretastatur und TextField vermieden.

Kreditkartenformatierung 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 sind Sie dafür verantwortlich, sowohl den neuen Text mit den Bindestrichen zu erstellen als auch zu berechnen, wie die Indexe 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 einfügen. Verwenden Sie dazu TextFieldBuffer aus dem Empfängerbereich von OutputTransformation.transformOutput.

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 Zuweisung des Werts 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 wird durch eine Schaltfläche die Markdown-Formatierung 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 zum Berechnen eines neuen TextFieldValue mit einem TextFieldState.edit-Aufruf.
    • Ein neues TextFieldValue wird generiert, indem der vorhandene Text basierend auf der aktuellen Auswahl zusammengefügt und die Markdown-Formatierungen dazwischen eingefügt werden.
    • Die Auswahl wird auch an neue 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 ähnlich wie bei onValueChange an.

ViewModel-Architektur StateFlow

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

In diesen Arten von Anwendungen wird 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-State-Hoisting-Paradigma verwenden. Bei der Texteingabe hat dieser Ansatz jedoch unvorhersehbare Nachteile. Die Probleme mit der tiefen Synchronisierung 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 UI-Status des ViewModel, das auf StateFlow basiert, kompatibel ist. 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 häufige Empfehlung ist, UI-Abhängigkeiten in einer ViewModel zu vermeiden. 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-State-System von Compose unterstützt werden. Sie unterscheiden sich nicht von Abhängigkeiten wie StateFlow oder RxJava. Daher empfehlen wir Ihnen,die Anwendung des Prinzips „Keine UI-Abhängigkeiten im ViewModel“ in Ihrem Code noch einmal zu überprüfen. Es ist nicht grundsätzlich schlecht, in Ihrem ViewModel auf ein TextFieldState zu verweisen.

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

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 ein paar TextFieldState-Werte.
  • Übergeben Sie diese TextFieldState-Objekte an TextFields in der LoginForm-Composable.

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 Verwendung der neuen TextFields. In diesem Fall können Sie weiterhin statusbasierte Textfelder verwenden, wenn Sie eine kleine Änderung vornehmen.

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

  • Behalten Sie die Klassen ViewModel und UiState bei.
  • Anstatt den Status direkt in ViewModel zu verschieben und ihn zur Quelle der Wahrheit für TextFields zu machen, sollten Sie ViewModel in einen einfachen Datencontainer umwandeln.
    • Dazu müssen Sie die Änderungen an jedem TextFieldState.text beobachten, indem Sie einen snapshotFlow in einem LaunchedEffect erfassen.
  • Für ViewModel werden weiterhin die neuesten Werte aus der Benutzeroberfläche verwendet, aber die uiState: StateFlow<UiState>-Werte werden nicht für die TextField-Werte verwendet.
  • Alle anderen Persistenzlogiken, die in Ihrem ViewModel implementiert sind, können unverändert bleiben.