На этой странице приведены примеры того, как можно перенести TextField на основе значений в TextField на основе состояний. Подробнее о различиях между TextField, основанными на значении и состоянии, рассказывается в статье Как настроить текстовые поля.
Основные функции
На основе ценности
@Composable fun OldSimpleTextField() { var state by rememberSaveable { mutableStateOf("") } TextField( value = state, onValueChange = { state = it }, singleLine = true, ) }
На уровне штата
@Composable fun NewSimpleTextField() { TextField( state = rememberTextFieldState(), lineLimits = TextFieldLineLimits.SingleLine ) }
- Замените
value, onValueChangeиremember { mutableStateOf("")наrememberTextFieldState(). - Замените
singleLine = trueнаlineLimits = TextFieldLineLimits.SingleLine.
Фильтрация по параметру "onValueChange"
На основе ценности
@Composable fun OldNoLeadingZeroes() { var input by rememberSaveable { mutableStateOf("") } TextField( value = input, onValueChange = { newText -> input = newText.trimStart { it == '0' } } ) }
На основе штата
@Preview @Composable fun NewNoLeadingZeros() { TextField( state = rememberTextFieldState(), inputTransformation = InputTransformation { while (length > 0 && charAt(0) == '0') delete(0, 1) } ) }
- Замените цикл обратного вызова значения на
rememberTextFieldState(). - Повторно реализуйте логику фильтрации в
onValueChangeс помощьюInputTransformation. - Используйте
TextFieldBufferиз области действия получателяInputTransformation, чтобы обновитьstate.InputTransformationвызывается сразу после того, как обнаруживается ввод пользователя.- Изменения, предложенные
InputTransformationчерезTextFieldBuffer, применяются немедленно, что позволяет избежать проблем с синхронизацией между виртуальной клавиатурой иTextField.
Форматирование номеров кредитных карт TextField
На основе ценности
@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 } } ) } ) }
На основе штата
@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, "-") }, ) }
- Замените фильтрацию в
onValueChangeнаInputTransformation, чтобы задать максимальную длину ввода.- Подробнее о фильтрации в
onValueChange…
- Подробнее о фильтрации в
- Замените
VisualTransformationнаOutputTransformation, чтобы добавить тире.- При использовании
VisualTransformationвы должны не только создать новый текст с дефисами, но и рассчитать, как индексы сопоставляются между визуальным текстом и состоянием поддержки. OutputTransformationавтоматически сопоставляет смещения. Вам нужно только добавить дефисы в правильные места, используяTextFieldBufferиз области видимости получателяOutputTransformation.transformOutput.
- При использовании
Обновление статуса (простой вариант)
На основе ценности
@Composable fun OldTextFieldStateUpdate(userRepository: UserRepository) { var username by remember { mutableStateOf("") } LaunchedEffect(Unit) { username = userRepository.fetchUsername() } TextField( value = username, onValueChange = { username = it } ) }
На основе штата
@Composable fun NewTextFieldStateUpdate(userRepository: UserRepository) { val usernameState = rememberTextFieldState() LaunchedEffect(Unit) { usernameState.setTextAndPlaceCursorAtEnd(userRepository.fetchUsername()) } TextField(state = usernameState) }
- Замените цикл обратного вызова значения на
rememberTextFieldState(). - Измените значение, присвоенное
TextFieldState.setTextAndPlaceCursorAtEnd.
Обновление статуса (сложный вариант)
На основе ценности
@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 ) }
На основе штата
@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) ) }
В этом случае кнопка добавляет элементы разметки Markdown, чтобы сделать текст вокруг курсора или текущего выделения полужирным. Он также сохраняет позицию выбора после изменений.
- Замените цикл обратного вызова значения на
rememberTextFieldState(). - Замените
maxLines = 10наlineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 10). - Измените логику вычисления нового значения
TextFieldValueс помощью вызоваTextFieldState.edit.- Новый объект
TextFieldValueсоздается путем разделения существующего текста на основе текущего выделения и вставки между ними элементов разметки Markdown. - Выделение также корректируется в соответствии с новыми индексами текста.
TextFieldState.editпредлагает более естественный способ редактирования текущего состояния с помощьюTextFieldBuffer.- Выделение явно определяет, куда вставлять декорации.
- Затем скорректируйте выделение, как в подходе
onValueChange.
- Новый объект
Архитектура ViewModel StateFlow
Многие приложения следуют рекомендациям по современной разработке приложений, которые поощряют использование StateFlow для определения состояния пользовательского интерфейса экрана или компонента через один неизменяемый класс, содержащий всю информацию.
В таких приложениях форма, например экран входа с текстовым вводом, обычно разрабатывается следующим образом:
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() ) } }
Такой дизайн идеально подходит для TextFields, в которых используется парадигма передачи состояния value,
onValueChange. Однако у этого подхода есть непредсказуемые недостатки, когда дело доходит до ввода текста. Проблемы с глубокой синхронизацией, возникающие при таком подходе, подробно описаны в записи блога Эффективное управление состоянием для TextField в Compose.
Проблема в том, что новый дизайн TextFieldState несовместим напрямую с состоянием пользовательского интерфейса ViewModel, поддерживаемым StateFlow. Замена username: String и password: String на username: TextFieldState и password: TextFieldState может показаться странной, поскольку TextFieldState по своей природе является изменяемой структурой данных.
Рекомендуется избегать размещения зависимостей пользовательского интерфейса в ViewModel.
Хотя в целом это хорошая практика, иногда она может быть неправильно истолкована.
Это особенно актуально для зависимостей Compose, которые являются чисто структурами данных и не несут в себе никаких элементов пользовательского интерфейса, таких как TextFieldState.
Классы, такие как MutableState или TextFieldState, представляют собой простые хранилища состояния, поддерживаемые системой состояния Snapshot в Compose. Они ничем не отличаются от зависимостей, таких как StateFlow или RxJava. Поэтому мы рекомендуем вам пересмотреть то,как вы применяете принцип "никаких зависимостей от пользовательского интерфейса в ViewModel" в своем коде. Сохранение ссылки на TextFieldState в ViewModel само по себе не является плохой практикой.
Рекомендуемый простой подход
Рекомендуем извлечь значения username или password из UiState и сохранить их в 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) } }
- Замените
MutableStateFlow<UiState>на несколько значенийTextFieldState. - Передайте эти объекты
TextFieldStateвTextFieldsв composable-функцииLoginForm.
Подход, соответствующий требованиям
Такие изменения в архитектуре не всегда легко реализовать. У вас может не быть возможности внести эти изменения, или затраты времени могут перевесить преимущества использования новых TextField. В этом случае вы все равно можете использовать
текстовые поля на основе состояния с небольшой настройкой.
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) } }
- Не меняйте классы
ViewModelиUiState. - Вместо того чтобы поднимать состояние непосредственно в
ViewModelи делать его источником истины дляTextFields, превратитеViewModelв простой держатель данных.- Для этого наблюдайте за изменениями каждого
TextFieldState.text, собираяsnapshotFlowвLaunchedEffect.
- Для этого наблюдайте за изменениями каждого
- В
ViewModelпо-прежнему будут последние значения из интерфейса, ноuiState: StateFlow<UiState>не будет влиять наTextField. - Другие логические элементы, реализованные в
ViewModel, можно оставить без изменений.