Na tej stronie znajdziesz przykłady migracji TextField
na podstawie wartości na TextField
na podstawie stanu. Informacje o różnicach między polami TextField
opartymi na wartościach a polami TextField
oparte na stanie 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 ) }
- Zamień fragment
value, onValueChange
iremember { mutableStateOf("")
narememberTextFieldState()
. - Zawartość komórki
singleLine = true
zastąp komórkąlineLimits = 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 z wartością przez
rememberTextFieldState()
. - Ponownie zaimplementuj logikę filtrowania w funkcji
onValueChange
, używając funkcjiInputTransformation
. - Aby zaktualizować
state
, użyj elementuTextFieldBuffer
w zakresie odbiornika elementuInputTransformation
.InputTransformation
jest wywoływany dokładnie po wykryciu interakcji użytkownika.- Zmiany zaproponowane przez
InputTransformation
za pomocąTextFieldBuffer
są stosowane natychmiast, co pozwala uniknąć problemów 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 } } ) } ) }
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 elementach
onValueChange
elementamiInputTransformation
, aby ustawić maksymalną długość danych wejściowych.- Zapoznaj się z sekcją Filtrowanie za pomocą funkcji
onValueChange
.
- Zapoznaj się z sekcją Filtrowanie za pomocą funkcji
- Zastąp
VisualTransformation
kodemOutputTransformation
, aby dodać łączniki.- W przypadku
VisualTransformation
jesteś odpowiedzialny za utworzenie nowego tekstu z łącznikami oraz za obliczenie mapowania indeksów między tekstem wizualnym a stanem podrzędnym. OutputTransformation
automatycznie zajmuje się mapowaniem przesunięcia. Musisz tylko dodać kreskę w odpowiednich miejscach, używającTextFieldBuffer
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 } ) }
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 wartością
rememberTextFieldState()
. - Zmień przypisanie wartości za pomocą
TextFieldState.setTextAndPlaceCursorAtEnd
.
Aktualizacja stanu (kompleksowa)
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 ozdobienia Markdown, aby pogrubić tekst wokół kursora lub bieżącego zaznaczenia. Utrzymuje też pozycję zaznaczenia po wprowadzeniu zmian.
- Zastąp pętlę wywołania z wartością przez
rememberTextFieldState()
. - Zawartość komórki
maxLines = 10
zastąp komórkąlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10)
. - Zmień logikę obliczania nowego parametru
TextFieldValue
za pomocą wywołania funkcjiTextFieldState.edit
.- Nowy element
TextFieldValue
jest generowany przez złączenie istniejącego tekstu na podstawie bieżącego zaznaczenia i wstawienie w środku znaków specjalnych Markdown. - Zaznaczenie jest też dostosowywane zgodnie z nowymi indeksami tekstu.
TextFieldState.edit
ma bardziej naturalny sposób edycji bieżącego stanu za pomocą funkcjiTextFieldBuffer
.- Zaznaczenie określa wyraźnie, gdzie wstawić dekoracje.
- Następnie dostosuj wybór w sposób podobny do
onValueChange
.
- Nowy element
Architektura StateFlow
ViewModel
Wiele aplikacji stosuje się do wytycznych dotyczących tworzenia nowoczesnych aplikacji, które zalecają używanie StateFlow
do definiowania stanu interfejsu użytkownika ekranu lub komponentu za pomocą jednej niezmiennej klasy zawierającej wszystkie informacje.
W takich aplikacjach formularz, np. ekran logowania z pola tekstowymi, 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() ) } }
Ta konstrukcja doskonale pasuje do TextFields
, które korzystają z paradygmatu value,
onValueChange
. W przypadku wprowadzania tekstu takie podejście ma jednak nieprzewidywalne wady. Problemy związane z głęboką synchronizacją w ramach tego podejścia są szczegółowo opisane w poście na blogu Skuteczne zarządzanie stanem w komponencie TextField w komponencie Compose.
Problem polega na tym, że nowy projekt TextFieldState
nie jest bezpośrednio zgodny ze stanem interfejsu użytkownika ViewModel z obsługą StateFlow
. Zastąpienie username: String
i password: String
wartościami username: TextFieldState
i password: TextFieldState
może wydawać się dziwne, ponieważ TextFieldState
to struktura danych, która może ulegać zmianom.
Zwykle zaleca się unikanie umieszczania zależności interfejsu użytkownika w ViewModel
.
Chociaż jest to ogólnie dobra praktyka, czasami może być źle interpretowana.
Dotyczy to zwłaszcza zależności w komponencie Compose, które są czysto strukturami danych i nie zawierają żadnych elementów interfejsu użytkownika, np. TextFieldState
.
Klasy takie jak MutableState
lub TextFieldState
to proste uchwyty stanu, które są obsługiwane przez system stanu migawek w Compose. Nie różnią się one od zależności takich jak StateFlow
czy RxJava
. Dlatego zachęcamy do ponownego przeanalizowania sposobu stosowania w kodziku zasady „brak zależności od interfejsu użytkownika w klasie ViewModel”. Nie jest złą praktyką umieszczanie w dokumentach ViewModel
odwołań do TextFieldState
.
Zalecane proste podejście
Zalecamy wyodrębnienie wartości takich jak username
lub password
z elementu UiState
i utworzenie dla nich osobnej referencji w elementach 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
doTextFields
w komponowalnym elemencieLoginForm
.
podejście zgodne z standardami,
Takie zmiany w architekturze nie zawsze są łatwe. Możesz nie mieć możliwości wprowadzenia tych zmian lub czas potrzebny na ich wdrożenie może przewyższać korzyści płynące z użycia nowych TextField
. W takim przypadku nadal możesz używać pól tekstowych zależnych od stanu, ale z niewielkimi zmianami.
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) } }
- Zajęcia
ViewModel
iUiState
pozostaną takie same. - Zamiast przenosić stan bezpośrednio do
ViewModel
i czynić go źródłem prawdy dlaTextFields
, zrób zViewModel
zwykłego posiadacza danych.- Aby to zrobić, obserwuj zmiany w każdym
TextFieldState.text
, zbierającsnapshotFlow
w komponencieLaunchedEffect
.
- Aby to zrobić, obserwuj zmiany w każdym
- Twoja
ViewModel
będzie nadal mieć najnowsze wartości z interfejsu użytkownika, ale jejuiState: StateFlow<UiState>
nie będzie sterowaćTextField
. - Inna logika trwałości zaimplementowana w Twoim
ViewModel
może pozostać bez zmian.