Na tej stronie znajdziesz przykłady migracji TextFieldów opartych na wartościach do TextFieldów opartych na stanach. Informacje o różnicach między elementami TextField opartymi na wartościach i stanach znajdziesz na stronie Konfigurowanie pól tekstowych.
Podstawowe użycie
Na podstawie wartości
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
Na podstawie stanu
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Zastąp
value, onValueChangeiremember { mutableStateOf("")wartościąrememberTextFieldState(). - Zastąp
singleLine = truetekstemlineLimits = TextFieldLineLimits.SingleLine.
Filtrowanie według: onValueChange
Na podstawie wartości
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
Na podstawie stanu
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Zastąp pętlę wywołania zwrotnego wartością
rememberTextFieldState(). - Ponownie zaimplementuj logikę filtrowania w
onValueChangeza pomocąInputTransformation. - Użyj
TextFieldBufferz zakresu odbiornikaInputTransformation, aby zaktualizowaćstate.- Funkcja
InputTransformationjest wywoływana natychmiast po wykryciu interakcji użytkownika. - Zmiany proponowane przez
InputTransformationza pomocąTextFieldBuffersą stosowane natychmiast, co pozwala uniknąć problemu z synchronizacją między klawiaturą ekranową aTextField.
- Funkcja
Formatowanie karty kredytowej TextField
Na podstawie wartości
@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 } } ) } ) }
Na podstawie stanu
@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, "-") }, ) }
- Zastąp filtrowanie w pliku
onValueChangeelementemInputTransformation, aby ustawić maksymalną długość danych wejściowych.- Zapoznaj się z sekcją Filtrowanie według
onValueChange.
- Zapoznaj się z sekcją Filtrowanie według
- Zastąp
VisualTransformationciągiemOutputTransformation, aby dodać myślniki.- W przypadku
VisualTransformationodpowiadasz zarówno za utworzenie nowego tekstu z kreskami, jak i za obliczenie sposobu mapowania indeksów między tekstem wizualnym a stanem bazowym. OutputTransformationautomatycznie zajmuje się mapowaniem przesunięcia. Wystarczy dodać kreski w odpowiednich miejscach, używającTextFieldBufferz zakresu odbiornikaOutputTransformation.transformOutput.
- W przypadku
Aktualizowanie stanu (proste)
Na podstawie wartości
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
Na podstawie stanu
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Zastąp pętlę wywołania zwrotnego wartością
rememberTextFieldState(). - Zmień przypisanie wartości za pomocą
TextFieldState.setTextAndPlaceCursorAtEnd.
Aktualizowanie stanu (złożone)
Na podstawie wartości
@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 ) }
Na podstawie stanu
@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) ) }
W tym przypadku przycisk dodaje znaczniki Markdown, aby pogrubić tekst wokół kursora lub bieżącego zaznaczenia. Po wprowadzeniu zmian zachowuje też pozycję zaznaczenia.
- Zastąp pętlę wywołania zwrotnego wartością
rememberTextFieldState(). - Zastąp
maxLines = 10tekstemlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Zmień logikę obliczania nowego elementu
TextFieldValueza pomocą wywołaniaTextFieldState.edit.- Nowy element
TextFieldValuejest generowany przez podzielenie istniejącego tekstu na podstawie bieżącego zaznaczenia i wstawienie między nimi dekoracji Markdown. - Zaznaczenie jest też dostosowywane do nowych indeksów tekstu.
TextFieldState.editma bardziej naturalny sposób edytowania bieżącego stanu za pomocąTextFieldBuffer.- Zaznaczenie jednoznacznie określa, gdzie należy wstawić dekoracje.
- Następnie dostosuj zaznaczenie, podobnie jak w przypadku
onValueChange.
- Nowy element
Architektura ViewModel StateFlow
Wiele aplikacji jest zgodnych z wytycznymi dotyczącymi tworzenia nowoczesnych aplikacji, które promują używanie StateFlow do definiowania stanu interfejsu ekranu lub komponentu za pomocą jednej niezmiennej klasy zawierającej wszystkie informacje.
W tego typu aplikacjach formularz taki jak ekran logowania z polem tekstowym jest zwykle zaprojektowany w ten sposób:
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() ) } }
Ten projekt idealnie pasuje do TextFields, które używają paradygmatu przenoszenia stanu value,
onValueChange. Jednak w przypadku wprowadzania tekstu to podejście ma nieprzewidywalne wady. Problemy z głęboką synchronizacją związane z tym podejściem zostały szczegółowo wyjaśnione w poście na blogu Skuteczne zarządzanie stanem TextField w Compose.
Problem polega na tym, że nowy projekt TextFieldState nie jest bezpośrednio zgodny ze stanem interfejsu ViewModel opartym na StateFlow. Zastąpienie username: String i password: String przez username: TextFieldState i password: TextFieldState może wydawać się dziwne, ponieważ TextFieldState jest z natury zmienną strukturą danych.
Często zaleca się unikanie umieszczania zależności interfejsu w ViewModel.
Chociaż jest to ogólnie dobra praktyka, czasami może być błędnie interpretowana.
Dotyczy to zwłaszcza zależności Compose, które są wyłącznie strukturami danych i nie zawierają żadnych elementów interfejsu, takich jak TextFieldState.
Klasy takie jak MutableState czy TextFieldState to proste zmienne stanu, które są obsługiwane przez system stanu Snapshot w Compose. Nie różnią się od zależności takich jak StateFlow czy RxJava. Dlatego zachęcamy do ponownej oceny sposobu stosowania w kodzie zasady „brak zależności interfejsu w ViewModel”. Przechowywanie odwołania do TextFieldState w ViewModel nie jest złą praktyką.
Zalecane proste podejście
Zalecamy wyodrębnienie wartości takich jak username lub password z UiState i zachowanie dla nich osobnego odwołania w ViewModel.
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) } }
- Zastąp
MutableStateFlow<UiState>kilkoma wartościamiTextFieldState. - Przekaż te obiekty
TextFieldStatedoTextFieldsw funkcji kompozycyjnejLoginForm.
Podejście zgodne
Tego typu zmiany architektoniczne nie zawsze są łatwe. Możesz nie mieć możliwości wprowadzenia tych zmian lub czas, który musisz na to poświęcić, może przewyższać korzyści płynące z używania nowych TextField. W takim przypadku możesz nadal używać pól tekstowych opartych na stanie, ale z niewielką modyfikacją.
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) } }
- Nie zmieniaj klas
ViewModeliUiState. - Zamiast przenosić stan bezpośrednio do
ViewModeli robić z niego źródło prawdy dlaTextFields, przekształćViewModelw prosty obiekt przechowujący dane.- Aby to zrobić, obserwuj zmiany w każdym
TextFieldState.text, zbierającsnapshotFlowwLaunchedEffect.
- Aby to zrobić, obserwuj zmiany w każdym
- W elemencie
ViewModelnadal będą widoczne najnowsze wartości z interfejsu, ale jegouiState: StateFlow<UiState>nie będzie wpływać naTextField. - Wszelka inna logika trwałości zaimplementowana w
ViewModelmoże pozostać bez zmian.