Поля ввода: подпись, ошибка и клавиатура
Зачем это нужно
«Создайте инпут» — один пункт критериев, а работы в нём больше, чем в кнопке: у поля ввода четыре состояния на макете, своя клавиатура под каждый тип данных и отдельный вопрос — когда показывать ошибку.
Последнее оценивается вживую. Если приложение ругается «неверная почта» на первую же набранную букву, это видно эксперту сразу и выглядит именно тем, чем является, — недоделкой.
Как и в прошлой главе, весь код собран настоящим компилятором, а сообщения об ошибках — его подлинный вывод.
Поле не хранит текст
Это первое, что ломает привычку тех, кто писал на вёрстке через XML. Поле ввода в Compose ничего не помнит. Оно получает строку в value, рисует её и сообщает о нажатиях через onValueChange. Хранить текст — обязанность вызывающего.
var email by remember { mutableStateOf("") }
KitTextField(
value = email,
onValueChange = { email = it },
label = "Почта",
)
А теперь то, ради чего эта схема тут:
@Composable
fun P() {
OutlinedTextField(value = "текст", onValueChange = {})
}
(сборка проходит, ошибок нет)
Компилятор доволен: типы сошлись. А поле не печатается — в него можно тыкать сколько угодно, текст не меняется, потому что value всегда одна и та же константа, а onValueChange ничего не делает.
«Поле не печатается» — самая частая жалоба на первом дне работы, и причина всегда одна: кольцо разомкнуто. Либо onValueChange не меняет состояние, либо в value уходит не то состояние, которое он меняет. Компилятор здесь не помощник: код законный.
Компонент библиотеки
@Composable
fun KitTextField(
value: String,
onValueChange: (String) -> Unit,
label: String,
modifier: Modifier = Modifier,
placeholder: String? = null,
error: String? = null,
enabled: Boolean = true,
singleLine: Boolean = true,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Next,
visualTransformation: VisualTransformation = VisualTransformation.None,
trailing: (@Composable () -> Unit)? = null,
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
singleLine = singleLine,
isError = error != null,
label = { Text(label) },
placeholder = placeholder?.let { { Text(it) } },
supportingText = error?.let { { Text(it) } },
trailingIcon = trailing,
visualTransformation = visualTransformation,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
)
}
Приём с error: String? вместо пары isError: Boolean и errorText: String стоит запомнить: он делает невозможным рассогласование. Нельзя случайно получить красную рамку без объяснения или объяснение без красной рамки.
Четыре состояния с макета
На макете поле обычно нарисовано четыре раза, и это не четыре компонента:
| Состояние | Что передаём |
|---|---|
| Пустое | value = "", placeholder = "ivan@mail.ru" |
| Заполненное | value = "oleg@pgk63.ru" |
| С ошибкой | error = "Неверный формат почты" |
| Отключённое | enabled = false |
Подпись (label) в Material 3 ведёт себя хитро: у пустого поля она стоит внутри как текст, а при вводе уезжает наверх и уменьшается. Подсказка (placeholder) видна только у пустого поля и только когда подпись уже уехала. Это частый источник вопроса «почему не видно placeholder» — он перекрыт подписью, пока в поле не встали курсором.
Пароль: то же поле, другие правила
@Composable
fun KitPasswordField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: String = "Пароль",
error: String? = null,
visible: Boolean = false,
trailing: (@Composable () -> Unit)? = null,
) {
KitTextField(
value = value,
onValueChange = onValueChange,
label = label,
modifier = modifier,
error = error,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
visualTransformation = if (visible) VisualTransformation.None else PasswordVisualTransformation(),
trailing = trailing,
)
}
Поле пароля — не новый компонент, а настройка существующего. Три отличия: своя клавиатура, Done вместо Next (пароль обычно последний) и преобразование показа — текст остаётся в состоянии как есть, а на экран выводятся точки.
visualTransformation меняет только вид. В value по-прежнему лежит настоящий пароль, и именно он уходит в проверку и в сеть. Путать эти две вещи опасно: попытка «взять то, что показано» даст строку из точек.
Кнопка «показать пароль» делается слотом — компонент про неё не знает:
KitPasswordField(
value = password,
onValueChange = { password = it },
visible = shown,
trailing = {
KitButtonGhost(text = if (shown) "Скрыть" else "Показать", onClick = { shown = !shown })
},
)
Когда показывать ошибку
Правило, которое решает большинство претензий на приёмке:
- пока человек ещё не уходил из поля — молчим, что бы он ни набрал;
- как только ушёл (нажал на другое поле, свернул клавиатуру) — проверяем и показываем;
- дальше проверяем на каждое изменение, чтобы ошибка исчезла сразу после починки.
Логику проверки удобно держать отдельно от разметки — её и протестировать проще:
fun pochtaOshibka(v: String): String? = when {
v.isBlank() -> "Укажите почту"
!v.contains("@") -> "В адресе нет знака @"
v.substringAfter("@").isBlank() -> "После @ ничего нет"
!v.substringAfter("@").contains(".") -> "В домене нет точки"
else -> null
}
fun main() {
for (v in listOf("", "oleg", "oleg@", "oleg@mail", "oleg@mail.ru")) {
println("\"" + v + "\" -> " + (pochtaOshibka(v) ?: "ок"))
}
}
А в разметке — только момент показа:
var email by remember { mutableStateOf("") }
var trogali by remember { mutableStateOf(false) }
val oshibka = if (trogali) pochtaOshibka(email) else null
KitTextField(
value = email,
onValueChange = { email = it },
label = "Почта",
error = oshibka,
modifier = Modifier.onFocusChanged { if (!it.isFocused) trogali = true },
)
Что ловит компилятор, а что нет
Подпись строкой вместо разметки:
OutlinedTextField(value = "", onValueChange = {}, label = "Почта")
e: Probe.kt:4:23 None of the following candidates is applicable:
То же самое сообщение, что и в главе про макет при fontSize = 16.dp, и читается так же: ни одна перегрузка не подошла, ищите свой параметр в сигнатуре. Там будет label: (@Composable () -> Unit)? — то есть нужна разметка, а не строка. Ровно так же отказывает supportingText = "ошибка".
Опечатка в перечислении:
KeyboardOptions(keyboardType = KeyboardType.Emal)
e: Probe.kt:5:75 Unresolved reference 'Emal'.
А это компилятор пропустит:
valueиз константы — поле не печатается;KeyboardType.Textтам, где нуженNumber— человек будет искать цифры на буквенной клавиатуре;imeAction = ImeAction.Nextна последнем поле формы — кнопка переведёт в никуда;singleLine = trueу поля адреса — длинный адрес не будет виден целиком.
Ни одна из четырёх последних ошибок не видна в коде — они видны только на устройстве. Поэтому каждое поле формы прокликивают руками: набрать, уйти, вернуться, отправить. Три минуты на форму.
Типичные ошибки
Ошибка 1. Состояние без remember.
Текст сбрасывается при каждой перерисовке: поле «печатает и стирает». Компилятор молчит.
Ошибка 2. Проверка прямо в разметке.
error = if (!email.contains("@")) "Неверно" else null
Ругань на первую букву. Нужен признак «трогали».
Ошибка 3. Ошибка не исчезает после починки.
Обратная крайность: проверили один раз при уходе и запомнили результат. Человек исправил адрес, а красная рамка висит. После первого показа проверять надо на каждое изменение.
Ошибка 4. Клавиатура не под тип данных.
Телефон, почта и количество набираются на разных клавиатурах. Это один параметр, и он оценивается.
Ошибка 5. Пароль виден в отладочном выводе.
Log.d("form", "пароль: $password")
Журнал показывают на демонстрации, а иногда прикладывают к сдаче.
Ошибка 6. trailing занят иконкой, а нужен для «показать пароль».
Слот один. Если туда уже положили иконку по макету, кнопку показа кладут в него же — вместе с иконкой в Row.
Шпаргалка по главе
| Что нужно | Как |
|---|---|
| Текст поля | хранить снаружи: var v by remember { mutableStateOf("") } |
| Связать | value = v, onValueChange = { v = it } |
| Подпись и подсказка | label = { Text(...) }, placeholder = { Text(...) } — разметка, не строка |
| Ошибка | один параметр error: String?, isError вывести внутри |
| Клавиатура | KeyboardOptions(keyboardType = ..., imeAction = ...) |
| Скрыть пароль | visualTransformation = PasswordVisualTransformation() |
| Момент проверки | признак «трогали» + Modifier.onFocusChanged |
| Проверить руками | набрать, уйти, вернуться, исправить, отправить |
Слова главы
| Термин | Что это |
|---|---|
| Управляемое поле | Поле, которое не хранит текст: получает value, отдаёт onValueChange |
label | Подпись; у пустого поля внутри, при вводе уезжает наверх |
placeholder | Подсказка; видна только у пустого поля |
supportingText | Текст под полем: пояснение или ошибка |
visualTransformation | Меняет только вид текста; в состоянии остаётся исходный |
imeAction | Что написано на кнопке клавиатуры: Next или Done |
| Признак «трогали» | Состояние, по которому решают, показывать ли ошибку |
Цель: скорость ≥ 95 зн/мин, точность ≥ 90%
var email by remember { mutableStateOf("") }
Что должен уметь после главы
- объяснить, почему поле не хранит текст, и починить «поле не печатается»;
- собрать компонент поля с подписью, подсказкой, ошибкой и клавиатурой;
- передать ошибку одним параметром и объяснить, почему не парой;
- показать четыре состояния поля одним компонентом;
- сделать поле пароля и не перепутать показанное со значением;
- выбрать момент показа ошибки и объяснить обе крайности;
- назвать, что здесь компилятор не проверяет вовсе.
Проверка по главе
Сводная проверка по всей главе: 10 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- Text fields в Compose — все параметры поля и их поведение.
- Настройка клавиатуры —
KeyboardOptions, типы и действия. - Состояние в Compose — почему текст живёт снаружи поля.
- Обработка фокуса —
onFocusChangedи порядок перехода между полями.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.