На этой странице приведены примеры миграции 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 . Однако при вводе текста у этого подхода есть непредсказуемые недостатки. Проблемы глубокой синхронизации, возникающие при таком подходе, подробно описаны в статье «Эффективное управление состоянием текстового поля в 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в компонуемом элементе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(usernameState) { snapshotFlow { usernameState.text.toString() }.collectLatest { loginViewModel.updatePassword(it) } } SecureTextField(passwordState) } }
- Сохраните классы
ViewModelиUiStateпрежними. - Вместо того, чтобы переносить состояние непосредственно в
ViewModelи делать его источником истины дляTextFields, превратитеViewModelв простой держатель данных.- Для этого наблюдайте за изменениями в каждом
TextFieldState.text, собираяsnapshotFlowвLaunchedEffect.
- Для этого наблюдайте за изменениями в каждом
- Ваша
ViewModelпо-прежнему будет иметь последние значения из UI, но ееuiState: StateFlow<UiState>не будет управлятьTextField. - Любая другая логика сохранения, реализованная в вашей
ViewModelможет остаться прежней.