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, 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 direkt nach Erkennen einer Nutzereingabe aufgerufen.- Änderungen, die von
InputTransformationüberTextFieldBuffervorgeschlagen werden, werden sofort angewendet. So wird ein Synchronisierungsproblem zwischen der Softwaretastatur undTextFieldvermieden.
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
onValueChangedurch einInputTransformation, 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.- Mit
VisualTransformationsind 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 dazuTextFieldBufferaus dem Empfängerbereich vonOutputTransformation.transformOutput.
- Mit
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 = 10durchlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Ändern Sie die Logik zum Berechnen eines neuen
TextFieldValuemit einemTextFieldState.edit-Aufruf.- Ein neues
TextFieldValuewird 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.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 ähnlich wie bei
onValueChangean.
- Ein neues
ViewModel-Architektur StateFlow
Viele Anwendungen folgen den Richtlinien für die moderne App-Entwicklung, die die Verwendung eines 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 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, wie z. B. TextFieldState.
Klassen wie MutableState oder TextFieldState sind einfache Status-Holder, die vom Snapshot-Status-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.
Empfohlene einfache Vorgehensweise
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 paarTextFieldState-Werte. - Übergeben Sie diese
TextFieldState-Objekte anTextFieldsin derLoginForm-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 TextField. 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
ViewModelundUiStatebei. - Anstatt den Status direkt in
ViewModelzu verschieben und ihn zur Quelle der Wahrheit fürTextFieldszu machen, sollten SieViewModelin einen einfachen Datencontainer umwandeln.- Dazu müssen Sie die Änderungen an jedem
TextFieldState.textbeobachten, indem Sie einensnapshotFlowin einemLaunchedEffecterfassen.
- Dazu müssen Sie die Änderungen an jedem
- Für
ViewModelwerden weiterhin die neuesten Werte aus der Benutzeroberfläche verwendet, aber dieuiState: StateFlow<UiState>-Werte werden nicht für dieTextField-Werte verwendet. - Alle anderen Persistenzlogiken, die in Ihrem
ViewModelimplementiert sind, können unverändert bleiben.