На этой странице приведены примеры того, как можно перенести TextField
s на основе значений в TextField
s на основе состояний. Информацию о различиях между TextField
s на основе значений и состояний см. на странице «Настройка текстовых полей» .
Основное использование
Ценностно-ориентированный
@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
. Однако у этого подхода есть непредсказуемые недостатки, когда дело доходит до ввода текста. Глубокие проблемы синхронизации с этим подходом подробно описаны в записи блога Effective state management for TextField in Compose .
Проблема в том, что новый дизайн TextFieldState
напрямую несовместим с состоянием ViewModel UI, поддерживаемым StateFlow
. Может показаться странным заменить username: String
и password: String
на username: TextFieldState
и password: TextFieldState
, поскольку TextFieldState
по своей сути является изменяемыми структурами данных.
Распространенная рекомендация — избегать размещения зависимостей UI в ViewModel
. Хотя это, как правило, хорошая практика, иногда ее можно неправильно истолковать. Это особенно верно для зависимостей Compose, которые являются чисто структурами данных и не несут с собой никаких элементов UI, таких как TextFieldState
.
Такие классы, как MutableState
или TextFieldState
являются простыми держателями состояний, которые поддерживаются системой состояний Compose's Snapshot. Они ничем не отличаются от зависимостей, таких как StateFlow
или RxJava
. Поэтому мы призываем вас пересмотреть то, как вы применяете принцип «никаких зависимостей UI в 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
s. В этом случае вы все еще можете использовать текстовые поля на основе состояния с небольшой настройкой.
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
может оставаться прежней.