Auf dieser Seite finden Sie Beispiele dafür, wie Sie wertbasierte TextField
s zu statusbasierten TextField
s migrieren können. Auf der Seite Textfelder konfigurieren finden Sie Informationen zu den Unterschieden zwischen wert- und zustandsbasierten TextField
s.
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
undremember { mutableStateOf("")
} durchrememberTextFieldState()
. - Ersetzen Sie
singleLine = true
durchlineLimits = 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 mitInputTransformation
. - Verwenden Sie
TextFieldBuffer
im Empfängerbereich vonInputTransformation
, um diestate
zu aktualisieren.InputTransformation
wird genau nach der Erkennung der Nutzereingabe aufgerufen.- Änderungen, die mit
InputTransformation
bisTextFieldBuffer
vorgeschlagen werden, werden sofort angewendet, um Synchronisierungsprobleme zwischen der Softwaretastatur undTextField
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 einInputTransformation
, um die maximale Länge der Eingabe festzulegen.- Weitere Informationen finden Sie im Abschnitt Durch
onValueChange
filtern.
- Weitere Informationen finden Sie im Abschnitt Durch
- Ersetzen Sie
VisualTransformation
durchOutputTransformation
, 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 desTextFieldBuffer
aus dem Empfängerbereich vonOutputTransformation.transformOutput
hinzufügen.
- Bei
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
durchlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Ändern Sie die Logik zum Berechnen einer neuen
TextFieldValue
mit einemTextFieldState.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 vonTextFieldBuffer
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
.
- Ein neuer
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.
Empfohlener einfacher Ansatz
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 mehrereTextFieldState
-Werte. - Übergeben Sie diese
TextFieldState
-Objekte in derLoginForm
-Komposition anTextFields
.
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 TextField
s. 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
- undUiState
-Klassen unverändert. - Anstatt den Status direkt in
ViewModel
zu platzieren und ihn zur Wahrheitsquelle fürTextFields
zu machen, machen SieViewModel
zu einem einfachen Datenhalter.- Beobachten Sie dazu die Änderungen an den einzelnen
TextFieldState.text
, indem Sie einensnapshotFlow
in einemLaunchedEffect
erfassen.
- Beobachten Sie dazu die Änderungen an den einzelnen
- Ihre
ViewModel
enthält weiterhin die neuesten Werte aus der Benutzeroberfläche, aber dieuiState: StateFlow<UiState>
steuert dieTextField
nicht. - Andere in Ihrem
ViewModel
implementierte Persistenzlogik kann unverändert bleiben.