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, onValueChange
iremember { mutableStateOf("")
} tekstemrememberTextFieldState()
. - Zastąp
singleLine = true
tekstemlineLimits = 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ą
rememberTextFieldState()
. - Zaimplementuj ponownie logikę filtrowania w
onValueChange
za pomocąInputTransformation
. - Użyj
TextFieldBuffer
z zakresu odbiornikaInputTransformation
, aby zaktualizowaćstate
.InputTransformation
jest wywoływana natychmiast po wykryciu interakcji użytkownika.- Zmiany proponowane przez
InputTransformation
za pomocąTextFieldBuffer
są stosowane natychmiast, co pozwala uniknąć problemu z synchronizacją między klawiaturą programową 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
onValueChange
znakiemInputTransformation
, aby ustawić maksymalną długość danych wejściowych.- Zapoznaj się z sekcją Filtrowanie za pomocą
onValueChange
.
- Zapoznaj się z sekcją Filtrowanie za pomocą
- Aby dodać łączniki, zastąp
VisualTransformation
kodemOutputTransformation
.- W przypadku
VisualTransformation
odpowiadasz zarówno za utworzenie nowego tekstu z myślnikami, jak i za obliczenie, jak indeksy są mapowane między tekstem wizualnym a stanem bazowym. OutputTransformation
automatycznie zajmuje się mapowaniem przesunięć. Wystarczy, że dodasz myślniki w odpowiednich miejscach, korzystając zTextFieldBuffer
z 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ą
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ą
rememberTextFieldState()
. - Zastąp
maxLines = 10
tekstemlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Zmień logikę obliczania nowego parametru
TextFieldValue
za pomocą wywołaniaTextFieldState.edit
.- Nowy
TextFieldValue
jest generowany przez połączenie 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.edit
ma 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 podnoszenia stanu value,
onValueChange
. W przypadku wpisywania tekstu takie podejście ma jednak nieprzewidywalne wady. Problemy z głęboką synchronizacją w tym podejściu zostały szczegółowo opisane w poście na blogu dotyczącym skutecznego zarządzania stanem komponentu TextField w Compose.
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 kontenery stanu, które są obsługiwane przez system stanu migawki 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łą praktyką.
Zalecane proste podejście
Zalecamy wyodrębnienie wartości takich jak username
lub password
z UiState
i przechowywanie ich w osobnym odwołaniu 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
TextFieldState
do funkcjiTextFields
w funkcjiLoginForm
composable.
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
ViewModel
iUiState
. - Zamiast przenosić stan bezpośrednio do komponentu
ViewModel
i ustawiać go jako źródło informacji oTextFields
, przekształćViewModel
w prosty kontener danych.- Aby to zrobić, obserwuj zmiany każdego
TextFieldState.text
, zbierającsnapshotFlow
wLaunchedEffect
.
- Aby to zrobić, obserwuj zmiany każdego
- Twój
ViewModel
bę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
ViewModel
mogą pozostać bez zmian.