Прямые ссылки на публичные уроки для быстрого старта и стабильной индексации lesson-страниц.
Практический урок по внедрению логики гипотетической регистрации. Акцент делаем на том, чтобы подружить между собой состояния composable функций. В качестве демонстрационной задачи добавим кнопку регистрации: по клику проверяем корректность введенной почты и отображаем результат. Дополнительные архитектурные слои не внедряем — задача научиться работать с Jetpack Compose.
Создаём отдельный файл RegistrationScreen с одноименной composable функцией. Переносим сюда функции PrimaryButton и CheckEmailField, вызовы из MainActivity заменяем на RegistrationScreen().
setContent { ComposePreviewTheme { Scaffold( content = { innerPadding -> Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(innerPadding) .fillMaxSize(), ) { RegistrationScreen() } } ) } }
@Composable fun RegistrationScreen() { Spacer(Modifier.height(70.dp)) StudyAppHeader( title = "Регистрация", subtitle = "Введите почту" ) Spacer(Modifier.height(200.dp)) CheckEmailField() Spacer(Modifier.height(30.dp)) CheckEmailButton() } @Composable @Preview fun PrimaryButton() { Button( shape = RoundedCornerShape(13.dp), onClick = {}, modifier = Modifier .height(56.dp) .padding(40.dp, 0.dp) .fillMaxWidth() ) { Text( "Зарегистрироваться", style = MaterialTheme.typography.labelMedium ) } } @Composable @Preview(showBackground = true) fun CheckEmailField() { var textState by remember { mutableStateOf("") } var errorState by remember { mutableStateOf("") } OutlinedTextField( modifier = Modifier .height(56.dp) .padding(40.dp, 0.dp) .fillMaxWidth(), value = textState, onValueChange = { textState = it errorState = if (EMAIL_ADDRESS.matcher(it).matches()) "" else "Некорректный email" }, isError = errorState.isNotEmpty(), label = { Text( text = if (errorState.isEmpty()) "Электропочта" else errorState, style = MaterialTheme.typography.headlineSmall, ) }, shape = RoundedCornerShape(13.dp), textStyle = MaterialTheme.typography.headlineMedium, placeholder = { Text( text = "example@androidsprint.ru", style = MaterialTheme.typography.headlineMedium, color = Color.Gray ) }, singleLine = true, trailingIcon = { IconButton( onClick = { textState = "" errorState = "" } ) { Icon( imageVector = Icons.Filled.Clear, contentDescription = "Иконка очистки поля" ) } } ) }
У функции Button есть параметр onClick с пустой лямбдой. Для начала добавим туда лог:
onClick = { Log.i("!!!", "PrimaryButton: нажата кнопка Зарегистрироваться") },
При разработке кнопок стоит стремиться делать их максимально "тупыми" — они должны только получать данные для отображения и отправлять события во вне, не зная о логике приложения. Что можно унифицировать? Передавать текст на кнопке и передавать событие клика во внешний мир через коллбэк.
Коллбэки позволяют устанавливать функции с отложенным выполнением — только по определённому событию. Здесь таким событием является клик пользователя.
Пошаговая установка коллбэка:
1. Добавляем параметр onRegisterClick: () -> Unit в функцию кнопки. Это лямбда, которая ничего не принимает и ничего не возвращает.
2. В месте вызова функции передаём onRegisterClick с нужной логикой:
PrimaryButton( text = "Зарегистрироваться", onRegisterClick = { Log.i("!!!", "PrimaryButton: нажата кнопка Зарегистрироваться") } )
3. Вызываем onRegisterClick() внутри параметра onClick.
Composable функции не возвращают значение, но благодаря коллбэкам мы можем передать сигнал о том, что некий код пора выполнять. Этим сигналом является нажатие на кнопку — оно же событие.
Коллбэк может не только сигнализировать, но и передавать данные. Например, если добавить в параметр строку, её можно будет передавать при возникновении события и получать через it:
PrimaryButton( text = "Зарегистрироваться", onRegisterClick = { it: String -> Log.i("!!!", "PrimaryButton: нажата кнопка $it") } )
Логика экрана предполагает три сценария:
"Регистрация успешно пройдена"."Некорректный email"."Такая почта уже существует".Для этого нужен доступ к textState и errorState, которые сейчас инкапсулированы внутри CheckEmailField. Нужно вынести логику наружу — это называется State Hoisting ("Поднятие состояния"). Официальные рекомендации Google доступны [по ссылке](https://developer.android.com/develop/ui/compose/state-hoisting#best-practice).
Выносим следующие параметры:
email: String — строка с почтой для отрисовки компонента.isEmailValid: Boolean — флаг валидности для параметра isError.onEmailChange: (String) -> Unit — коллбэк ввода, передаёт строку при каждом вводе символа.onClearClicked: () -> Unit — коллбэк очистки поля.Задействуем новые параметры внутри функции:
email → value для визуального отображения.!isEmailValid && email.isNotBlank(). В label проверяем валидность и показываем текст ошибки.onEmailChange — отправляем строку при каждом вводе символа.onClearClicked — очищаем стейты по клику на иконку.Вы должны поднять состояние пользовательского интерфейса до наименьшего общего предка между всеми компонентами, которые его читают и записывают. — документация Google
Теперь CheckEmailField только принимает данные и бросает коллбэки. Стейты переносим в начало файла:
var userEmail by remember { mutableStateOf("") } var isEmailFormatValid by remember { mutableStateOf(true) } var validationMessage by remember { mutableStateOf("") }
validationMessage — стейт для хранения итогового сообщения регистрации. Он будет меняться динамически в зависимости от условий и содержания других стейтов.
Заполняем параметры CheckEmailField:
email передаём userEmail.isEmailValid — isEmailFormatValid.onEmailChange: присваиваем строку стейту, проводим валидацию, записываем результат в isEmailFormatValid. Если невалидна — записываем предупреждение в validationMessage, иначе пустую строку.onClearClicked: очищаем все текстовые стейты, isEmailFormatValid возвращаем в true.В onRegisterClick заполняем validationMessage по цепочке условий:
"Некорректный email"."Такая почта уже существует"."Регистрация успешно пройдена".PrimaryButton( text = "Зарегистрироваться", onRegisterClick = { it: String -> validationMessage = if (userEmail.isEmpty() || !isEmailFormatValid) { "Некорректный email" } else if (userEmail == testEmail) { "Такая почта уже существует" } else { "Регистрация успешно пройдена" } Log.i("!!!", "PrimaryButton: нажата кнопка $it") } )
Добавляем Text с текстом из validationMessage. Цвет текста проверяем по наличию слова "успешно" — это упрощённый подход, в продакшне лучше использовать отдельный стейт для цвета или привязывать его к стейтам успеха/ошибки.
Чтобы не скрывать компонент условием, используем модификатор alpha, который управляет прозрачностью: 0f — невидим, 1f — полностью виден. Компонент остаётся на экране физически.
Spacer(Modifier.height(30.dp)) Text( text = validationMessage, style = MaterialTheme.typography.bodyLarge, color = if (validationMessage.contains("успешно")) Color.DarkGray else Color.Red, modifier = Modifier.alpha(if (validationMessage.isNotEmpty() && isEmailFormatValid) 1f else 0f) )
Демонстрационный экран готов. Это упрощённая реализация без продакшн-архитектуры — задача была показать, как работают стейты и коллбэки в рамках Jetpack Compose.
В следующем уроке рассмотрим реализацию ленивых списков.