На этой странице приведены примеры того, как можно перевести 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из области видимости Receiver объекта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по-прежнему будет содержать самые актуальные значения из пользовательского интерфейса, но еёuiState: StateFlow<UiState>не будет управлятьTextField. - Любая остальная логика сохранения данных, реализованная в вашей
ViewModelможет оставаться без изменений.