Na tej stronie znajdziesz przykłady migracji TextField opartych na wartościach do TextField opartych na stanie. Więcej informacji o różnicach między 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, ) }
Zależne od stanu
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Zastąp
value, onValueChangeiremember { mutableStateOf("")} tekstemrememberTextFieldState(). - 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' } } ) }
Zależne od 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 zmienną
rememberTextFieldState(). - Ponownie zaimplementuj logikę filtrowania w
onValueChangeza pomocąInputTransformation. - Użyj
TextFieldBufferz zakresu odbiornikaInputTransformation, aby zaktualizowaćstate.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.
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 } } ) } ) }
Zależne od 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
onValueChangeznakiemInputTransformation, aby ustawić maksymalną długość danych wejściowych.- Zapoznaj się z sekcją Filtrowanie za pomocą ikony
onValueChange.
- Zapoznaj się z sekcją Filtrowanie za pomocą ikony
- Aby dodać myślniki, zastąp
VisualTransformationciągiemOutputTransformation.- W przypadku
VisualTransformationodpowiadasz zarówno za utworzenie nowego tekstu z myślnikami, jak i za obliczenie, jak indeksy są mapowane między tekstem wizualnym a stanem bazowym. OutputTransformationautomatycznie zajmuje się mapowaniem przesunięć. Wystarczy, że dodasz myślniki w odpowiednich miejscach, korzystając zTextFieldBufferz 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 } ) }
Zależne od 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 zmienną
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 ) }
Zależne od 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 dekoracje Markdown, aby pogrubić tekst wokół kursora lub bieżącego zaznaczenia. Zachowuje też pozycję zaznaczenia po wprowadzeniu zmian.
- Zastąp pętlę wywołania zwrotnego wartości zmienną
rememberTextFieldState(). - Zastąp
maxLines = 10tekstemlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Zmień logikę obliczania nowego parametru
TextFieldValueza pomocą wywołaniaTextFieldState.edit.- Nowy
TextFieldValuejest generowany przez połączenie istniejącego tekstu na podstawie bieżącego zaznaczenia i wstawienie dekoracji Markdown między nimi. - Zaznaczenie jest też dostosowywane do nowych indeksów tekstu.
TextFieldState.editma bardziej naturalny sposób edytowania bieżącego stanu za pomocąTextFieldBuffer.- Wybór wyraźnie określa, gdzie wstawić dekoracje.
- Następnie dostosuj wybór podobnie jak w przypadku metody
onValueChange.
- Nowy
Architektura ViewModel StateFlow
Wiele aplikacji jest zgodnych z wytycznymi dotyczącymi nowoczesnego tworzenia aplikacji, które zalecają używanie StateFlow do definiowania stanu interfejsu ekranu lub komponentu za pomocą jednej niezmiennej klasy zawierającej wszystkie informacje.
W tego typu aplikacjach formularz, np. 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 korzystają z paradygmatu value,
onValueChange. W przypadku wpisywania tekstu takie podejście ma jednak nieprzewidywalne wady. Problemy z głęboką synchronizacją w przypadku tego podejścia zostały szczegółowo opisane w tym poście na blogu.
Problem polega na tym, że nowy projekt TextFieldState nie jest bezpośrednio zgodny ze stanem interfejsu StateFlow ViewModel. Zastąpienie username: String i password: String znakami username: TextFieldState i password: TextFieldState może wydawać się dziwne, ponieważ TextFieldState jest z natury strukturą danych, którą można modyfikować.
Często zaleca się unikanie umieszczania zależności interfejsu w ViewModel.
Chociaż jest to na ogół dobra praktyka, czasami może być źle interpretowana.
Dotyczy to zwłaszcza zależności Compose, które są czysto danymi i nie zawierają żadnych elementów interfejsu, takich jak TextFieldState.
Klasy takie jak MutableState czy TextFieldState to proste obiekty przechowujące stan, które są obsługiwane przez system stanu Snapshot w Compose. Nie różnią się one 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 samo w sobie złym rozwiązaniem.
Zalecane proste podejście
Zalecamy wyodrębnienie wartości takich jak username lub password z UiState i przechowywanie ich w osobnym pliku referencyjnym 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
TextFieldStatedo funkcjiTextFieldsw funkcjiLoginFormkompozycyjnej.
Podejście zgodne
Takie zmiany architektury nie zawsze są łatwe. Możesz nie mieć możliwości wprowadzenia tych zmian lub czas potrzebny na ich wprowadzenie może przewyższać korzyści z używania nowych TextField. W takim przypadku możesz nadal używać pól tekstowych opartych na stanie, ale z niewielką zmianą.
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) } }
- Zachowaj te same zajęcia
ViewModeliUiState. - Zamiast przenosić stan bezpośrednio do komponentu
ViewModeli ustawiać go jako źródło informacji o komponencieTextFields, przekształćViewModelw prosty kontener danych.- Aby to zrobić, obserwuj zmiany każdego
TextFieldState.text, zbierającsnapshotFlowwLaunchedEffect.
- Aby to zrobić, obserwuj zmiany każdego
- Twój
ViewModelbędzie nadal zawierać najnowsze wartości z interfejsu, ale jegouiState: StateFlow<UiState>nie będzie wpływać naTextField. - Wszelkie inne mechanizmy utrwalania danych zaimplementowane w
ViewModelmogą pozostać bez zmian.