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, onValueChangeundremember { mutableStateOf("")} durchrememberTextFieldState(). - Ersetzen Sie
singleLine = truedurchlineLimits = 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
onValueChangemitInputTransformationneu. - Verwenden Sie
TextFieldBufferaus dem Empfängerbereich vonInputTransformation, umstatezu aktualisieren.InputTransformationwird genau dann aufgerufen, wenn eine Nutzereingabe erkannt wird.- Änderungen, die von
InputTransformationüberTextFieldBuffervorgeschlagen werden, werden sofort angewendet, um ein Synchronisierungsproblem zwischen der Softwaretastatur undTextFieldzu 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
onValueChangedurch einenInputTransformation, um die maximale Länge der Eingabe festzulegen.- Weitere Informationen finden Sie im Abschnitt Filtern über
onValueChange.
- Weitere Informationen finden Sie im Abschnitt Filtern über
- Ersetzen Sie
VisualTransformationdurchOutputTransformation, um Bindestriche hinzuzufügen.- Bei
VisualTransformationsind 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 demTextFieldBufferaus dem Empfängerbereich vonOutputTransformation.transformOutputeinfü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 ü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 = 10durchlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Ändern Sie die Logik zur Berechnung eines neuen
TextFieldValuemit einemTextFieldState.edit-Aufruf.- Ein neuer
TextFieldValuewird 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.editbietet eine natürlichere Möglichkeit, den aktuellen Status mitTextFieldBufferzu bearbeiten.- Die Auswahl definiert explizit, wo die Dekorationen eingefügt werden sollen.
- Passen Sie dann die Auswahl an, ähnlich wie beim
onValueChange-Ansatz.
- Ein neuer
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.
Empfohlener einfacher Ansatz
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 einigeTextFieldState-Werte. - Übergib diese
TextFieldState-Objekte anTextFieldsin der zusammensetzbaren FunktionLoginForm.
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
ViewModelundUiStatebei. - Anstatt den Status direkt in
ViewModelzu verschieben und ihn zur „Source of Truth“ fürTextFieldszu machen, wandeln SieViewModelin einen einfachen Datenhalter um.- Beobachten Sie dazu die Änderungen an jedem
TextFieldState.text, indem Sie einensnapshotFlowin einemLaunchedEffecterfassen.
- Beobachten Sie dazu die Änderungen an jedem
- Ihre
ViewModelenthält weiterhin die neuesten Werte aus der Benutzeroberfläche, aber dieuiState: StateFlow<UiState>wird nicht für dieTextFields verwendet. - Alle anderen Persistenzlogiken, die in Ihrer
ViewModelimplementiert sind, können unverändert bleiben.