Android Studio: уроки на Kotlin. Экран регистрации

Нет лучшего способа освоить современную Android-разработку, чем написать что-то руками. В этом уроке из серии Android Studio: уроки на Kotlin мы создадим полноценный экран регистрации с проверкой вводимых данных, используя Jetpack Compose и Kotlin. Вы узнаете, как управлять состоянием полей, реализовать валидацию на лету, показывать ошибки и обрабатывать нажатие кнопки — и всё это в декларативном стиле, без XML.

Что мы будем строить и зачем

Цель урока — экран с тремя полями: имя, email и пароль. После нажатия кнопки «Зарегистрироваться» система проверяет корректность ввода и либо показывает приветствие, либо подсвечивает ошибочные поля с пояснениями. Такой компонент есть в каждом втором приложении, поэтому понимание его внутреннего устройства критически важно для любого, кто изучает Android Studio Kotlin уроки на практике.


Шаг 1: Создаём проект в Android Studio

Запустите Android Studio и выберите New Project. В шаблонах выберите Empty Activity и дайте проекту имя, например, RegistrationScreen. Убедитесь, что в качестве языка выбран Kotlin, а минимальный SDK установлен на API 24 или выше. Готовый шаблон уже содержит одну MainActivity и базовую настройку Compose — нам остаётся только наполнить его логикой.


Шаг 2: Знакомимся с состоянием в Compose

В Jetpack Compose интерфейс не обновляется напрямую — он перерисовывается при изменении состояния. Для хранения состояния отдельного поля мы будем использовать remember { mutableStateOf(...) }. Это гарантирует, что значение сохранится между перекомпоновками и его изменение вызовет перерисовку только тех частей экрана, которые от него зависят.


Переменные состояния для полей

Объявим три переменные внутри нашей компонуемой функции:

var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }

Ключевое слово by позволяет работать с состоянием как с обычной переменной, а делегат remember сохраняет его при рекомпозиции.


Шаг 3: Верстаем экран регистрации

Расположим поля и кнопку в вертикальной колонке с отступами. Каждое текстовое поле обернём в OutlinedTextField — стандартный компонент Material 3. Не забудем про лейблы и визуальные разделители.

Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(24.dp),
    verticalArrangement = Arrangement.Center
) {
    Text("Регистрация", style = MaterialTheme.typography.headlineMedium)
    Spacer(modifier = Modifier.height(16.dp))
    OutlinedTextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Имя") },
        modifier = Modifier.fillMaxWidth()
    )
    Spacer(modifier = Modifier.height(8.dp))
    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = { Text("Email") },
        modifier = Modifier.fillMaxWidth()
    )
    Spacer(modifier = Modifier.height(8.dp))
    OutlinedTextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Пароль") },
        visualTransformation = PasswordVisualTransformation(),
        modifier = Modifier.fillMaxWidth()
    )
    Spacer(modifier = Modifier.height(16.dp))
    Button(
        onClick = { /* скоро добавим */ },
        modifier = Modifier.fillMaxWidth()
    ) {
        Text("Зарегистрироваться")
    }
}

Для пароля используем PasswordVisualTransformation(), чтобы скрыть символы. Импорт этой функции уже есть в Compose.


Шаг 4: Валидация полей — умная и наглядная

Теперь самое интересное. Нам нужна логика, которая проверяет поля при нажатии кнопки и запоминает ошибки. Для этого заведём ещё одну переменную состояния — errorMessages — и будем наполнять её при попытке регистрации.

var errorMessages by remember { mutableStateOf(mapOf<String, String>()) }

Ключами будут названия полей (name, email, password), а значениями — текст ошибки. При каждом нажатии кнопки мы сбрасываем предыдущие ошибки и формируем новый словарь.


Функция валидации

Напишем отдельную функцию, которая возвращает true, только если все поля прошли проверку:

fun validate(): Boolean {
    val errors = mutableMapOf<String, String>()
    if (name.isBlank()) errors["name"] = "Имя обязательно"
    if (email.isBlank() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches())
        errors["email"] = "Введите корректный email"
    if (password.length < 6) errors["password"] = "Пароль должен быть не менее 6 символов"
    errorMessages = errors.toMap()
    return errors.isEmpty()
}

Обратите внимание: toMap() копирует данные, чтобы гарантировать изменение состояния.


Шаг 5: Показываем ошибки под полями

Каждому OutlinedTextField можно передать параметр isError и дополнительный слот supportingText. Если для поля есть ошибка, мы включим флаг и выведем текст.

OutlinedTextField(
    value = name,
    onValueChange = { name = it; errorMessages = errorMessages - "name" },
    isError = errorMessages.containsKey("name"),
    supportingText = { errorMessages["name"]?.let { Text(it) } },
    // остальные параметры
)

Когда пользователь начинает вводить символ в ошибочное поле, мы сразу убираем сообщение для этого поля — простой, но очень дружелюбный UX.


Шаг 6: Обработка успешной регистрации

Внутри onClick кнопки вызовем validate(). Если метод вернул true, покажем приветственный Snackbar с помощью SnackbarHostState и корутины. Для этого добавим val snackbarHostState = remember { SnackbarHostState() } и обернём Scaffold с snackbarHost.

val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { paddingValues ->
    // контент с paddingValues
    Button(onClick = {
        if (validate()) {
            scope.launch {
                snackbarHostState.showSnackbar("Добро пожаловать, $name!")
            }
        }
    }) { ... }
}

Теперь ошибки отображаются только при некорректном вводе, а в случае успеха мы приветствуем пользователя по имени.


Полный код MainActivity

Чтобы вам было проще, вот итоговая версия экрана регистрации. Скопируйте её в свой проект и запустите.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                RegistrationScreen()
            }
        }
    }
}

@Composable
fun RegistrationScreen() {
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var errorMessages by remember { mutableStateOf(mapOf<String, String>()) }
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    fun validate(): Boolean {
        val errors = mutableMapOf<String, String>()
        if (name.isBlank()) errors["name"] = "Имя обязательно"
        if (email.isBlank() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches())
            errors["email"] = "Введите корректный email"
        if (password.length < 6) errors["password"] = "Пароль должен быть не менее 6 символов"
        errorMessages = errors.toMap()
        return errors.isEmpty()
    }

    Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
                .padding(24.dp),
            verticalArrangement = Arrangement.Center
        ) {
            Text("Регистрация", style = MaterialTheme.typography.headlineMedium)
            Spacer(modifier = Modifier.height(16.dp))
            OutlinedTextField(
                value = name,
                onValueChange = { name = it; errorMessages = errorMessages - "name" },
                isError = errorMessages.containsKey("name"),
                supportingText = { errorMessages["name"]?.let { Text(it) } },
                label = { Text("Имя") },
                modifier = Modifier.fillMaxWidth()
            )
            Spacer(modifier = Modifier.height(8.dp))
            OutlinedTextField(
                value = email,
                onValueChange = { email = it; errorMessages = errorMessages - "email" },
                isError = errorMessages.containsKey("email"),
                supportingText = { errorMessages["email"]?.let { Text(it) } },
                label = { Text("Email") },
                modifier = Modifier.fillMaxWidth()
            )
            Spacer(modifier = Modifier.height(8.dp))
            OutlinedTextField(
                value = password,
                onValueChange = { password = it; errorMessages = errorMessages - "password" },
                isError = errorMessages.containsKey("password"),
                supportingText = { errorMessages["password"]?.let { Text(it) } },
                label = { Text("Пароль") },
                visualTransformation = PasswordVisualTransformation(),
                modifier = Modifier.fillMaxWidth()
            )
            Spacer(modifier = Modifier.height(16.dp))
            Button(
                onClick = {
                    if (validate()) {
                        scope.launch {
                            snackbarHostState.showSnackbar("Добро пожаловать, $name!")
                        }
                    }
                },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Зарегистрироваться")
            }
        }
    }
}

Чему вы научились

Этот урок из цикла Android Studio: Kotlin уроки дал вам практические навыки работы с состоянием в Jetpack Compose, валидацией форм, отображением динамических ошибок и использованием Snackbar. Вы написали чистое, структурированное приложение, которое можно сразу положить в портфолио. Понимание этих основ открывает дорогу к созданию более сложных экранов — авторизации, профилей, настроек — и формирует правильное мышление декларативного UI.