Auf dieser Seite finden Sie Beispiele für die Migration von wertbasierten TextField
s zu zustandsbasierten TextField
s. Informationen zu den Unterschieden zwischen wert- und statusbasierten TextField
s 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
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
mitInputTransformation
neu. - Verwenden Sie
TextFieldBuffer
aus dem Empfängerbereich vonInputTransformation
, umstate
zu aktualisieren.InputTransformation
wird direkt nach der Erkennung einer Nutzereingabe aufgerufen.- Änderungen, die von
InputTransformation
überTextFieldBuffer
vorgeschlagen werden, werden sofort angewendet. So wird ein Synchronisierungsproblem zwischen der Softwaretastatur undTextField
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 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
VisualTransformation
durchOutputTransformation
, 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 dazuTextFieldBuffer
aus dem Empfängerbereich vonOutputTransformation.transformOutput
.
- 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 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
durchlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Ändern Sie die Logik zum Berechnen eines neuen
TextFieldValue
mit einemTextFieldState.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 mitTextFieldBuffer
zu bearbeiten.- Die Auswahl definiert explizit, wo die Dekorationen eingefügt werden sollen.
- Passen Sie dann die Auswahl ähnlich wie bei
onValueChange
an.
- Ein neues
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.
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 anTextFields
in 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
s. 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
undUiState
bei. - Anstatt den Status direkt in
ViewModel
zu verschieben und ihn zur Quelle der Wahrheit fürTextFields
zu machen, sollten SieViewModel
in einen einfachen Datencontainer umwandeln.- Dazu müssen Sie die Änderungen an jedem
TextFieldState.text
beobachten, indem Sie einensnapshotFlow
in einemLaunchedEffect
erfassen.
- Dazu müssen Sie die Änderungen an jedem
- Für
ViewModel
werden 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
ViewModel
implementiert sind, können unverändert bleiben.