Урок 10: Коллбэки (callback). Внедрение логики регистрации

Урок 10: Коллбэки (callback). Внедрение логики регистрации

Введение

Сейчас будет максимально практический урок. Будем внедрять логику нашей гипотетической регистрации. Акцент в первую очередь делаем на том, чтобы подружить между собой состояния composable функций. На текущий момент мы изучили состояния и как происходит рекомпозиция. А на предыдущем уроке создали классное анимируемое и валидируемое текстовое поле для ввода электронной почты.

Продолжим. В качестве демонстрационной задачи мы добавим кнопку регистрации. По клику будем проверять корректность введенной почты и отображать результат проверки. Очень распространенный функционал, а самое главное напичкан интересной и полезной логикой. Дополнительные архитектурные слои не внедряем, сейчас задача научиться работать с Jetpack Compose.

Подготовка нового экрана

Создам под все это дело отдельный файл RegistrationScreen с одноименной composable функцией внутри. Ее мы будем вызывать в MainActivity для отладки и тестирования в процессе разработки.

Теперь перенесу сюда разрабатываемые ранее функции 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 = "ex*****@***********nt.ru",
                style = MaterialTheme.typography.headlineMedium,
                color = Color.Gray
            )
        },
        singleLine = true,
        trailingIcon = {
            IconButton(
                onClick = {
                    textState = ""
                    errorState = ""
                }
            ) {
                Icon(
                    imageVector = Icons.Filled.Clear,
                    contentDescription = "Иконка очистки поля"
                )
            }
        }

    )
}

Добавление коллбэка на кнопку / callback

Для начала добавим CheckEmailButton(), чтобы наконец она отображалась на экране и рассмотрим ее более внимательно. Нам нужно, чтобы выполнялось какое-то действие при клике. Для этого у функции Button есть параметр onClick с пока что пустой лямбдой. Начнем с самого простого, просто добавим туда лог, который будет имитировать какую-то логику. Пусть печатается в консоль текст с названием кнопки “нажата кнопка Зарегистрироваться”.

onClick = {
    Log.i("!!!", "PrimaryButton: нажата кнопка Зарегистрироваться")
},
Урок 10: Коллбэки (callback). Внедрение логики регистрации

Отлично, в логкате все отбивается как положено. О чем я должен думать при разработке кнопок. Я предполагаю, что кнопок в таком дизайне у меня может быть много (особенно, если я их вижу в дизайне перед глазами). Следовательно я должен стремиться сделать их максимально “тупыми”. То есть кнопки по возможности должны максимально переиспользоваться и не должны знать о том, какая логика будет происходить у них внутри.

То есть они (как и другие переиспользуемые функции) должны только получать какие-то данные для отображения, а также отправлять события во вне. Что мы можем тут унифицировать? Самое очевидное – это передавать текст на кнопке. На втором месте это событие, которое происходит в момент нажатия на кнопку. То есть мы можем передавать сам клик из функции во внешний мир.

Воспользуемся возможностью добавлять в функцию любое количество параметров. Для начала определим параметр с текстом и будем присваивать его функции текст. Отправлю этот параметр в месте вызова функции. Проверяем и все хорошо. Тут все должно быть понятно. Интереснее дальше.

Вот все, что написано в лямбде onClick – это какая-то логика, которая тут быть не должна. Я хочу, чтобы она выполнялась в другом месте. И мы можем создать коллбэк (или функцию обратного вызова).

Что такое коллбэк / callback

С помощью коллбэков можно устанавливать функции с отложенным выполнением, то есть только по определенному событию. Этим событием сейчас выступает клик пользователя.

Пошаговая установка коллбэка:

  • Добавляем новый параметр в функцию с кнопкой onRegisterClick: () -> Unit,. Это обычный параметр, но не примитивного типа, а который является лямбда-функцией. Эта лямбда ничего не принимает и ничего не возвращает.
  • Далее в месте вызова функции дописываю недостающий параметр onRegisterClick и в фигурные скобки (потому, что это фактически лямбда) переношу логику создания лога. Предполагаем, что там какая-то логика приложения и мы не хотим, чтобы кнопка о ней знала.
PrimaryButton(
    text = "Зарегистрироваться",
    onRegisterClick = {
        Log.i("!!!", "PrimaryButton: нажата кнопка Зарегистрироваться")
    }
)
  • Наконец, просто добавляю вызов созданного парамета onRegisterClick() в параметр onClick. Сначала убедимся, что все работает. И расскажу еще раз, что мы сделали.

Мы создали коллбэк (или функцию обратного вызова) onRegisterClick и теперь при клике на кнопку вызываем создание логов за пределами функции PrimaryButton.

Урок 10: Коллбэки (callback). Внедрение логики регистрации

Помните, я говорил, что компоуз функции не могут возвращать значение. И это действительно так, не нужно путать эти концепции. Но благодаря коллбэкам мы можем передать СИГНАЛ о том, что некий код пора выполнять. То есть сигналом здесь выступает нажатие пользователем на кнопку, оно же является событием. Это более распространенное название. Мы передаем это событие с помощью коллбэка.

Важное дополнение. Хоть это и сигнал, который оповещает удаленную функцию о том, что пора выполниться – коллбэк может еще и передавать данные. Ну потому, что так работает лямбда, тут уже простая логика и функционал, который ограничивается вашей фантазией или бизнес требованиями.

Например, если добавить в параметр коллбэка строку на вход в лямбду, эту строку можно будет передавать в момент возникновения события. Буду передавать название кнопки и в лямбде это теперь можно получить через it.

PrimaryButton(
    text = "Зарегистрироваться",
    onRegisterClick = { it: String ->
        Log.i("!!!", "PrimaryButton: нажата кнопка $it")
    }
)

Вынос параметров

Отлично. Теперь надо определиться что будет происходить в этом блоке согласно нашей логике. Предлагаю следующую реализацию:

  • Пользователь вводит почту, нажимает кнопку и отображается сообщение “Регистрация успешно пройдена”. То есть нам понадобится функция Text.
  • Если форма пустая или введен любой текст, отличный от почты – отображаем сообщение “Некорректный email".
  • Если введенная почта совпадает с некой сохраненной тестовой почтой – отображаем сообщение "Такая почта уже существует".

Какой вывод напрашивается? Слишком много всего нужно здесь обрабатывать. Это и textState, и тот же errorState. Все эти данные сейчас инкапсулированы внутри функции CheckEmailField. Следовательно для начала нужно провести аналогичную процедуру по выносу логики из CheckEmailField. Это сильно упростит нам работу. Кстати, эта процедура называется State Hoist – “Поднятие Состояния”. И есть даже официальные рекомендации от гугл.

Стейты, которые сейчас работают только на CheckEmailField будут общими для текстового поля, кнопки и функции текста для отображения результатов регистрации. Ведь это единая логика экрана, как единый организм.

Нам нужно вынести следующие параметры:

  • email: String, – строка с почтой. Нужно для отрисовки компонента, потому, что логика валидации будет вынесена за пределы метода.
  • isEmailValid: Boolean, – флаг, получаемый из стейта с ошибкой, сделаем более понятное название у него и у стейта чуть позже. Нужен для передачи статуса в параметр isError.
  • onEmailChange: (String) -> Unit, – коллбэк ввода, который принимает строку и передает ее при каждом вводе символа пользователя. Нужно для передачи строки на валидацию.
  • onClearClicked: () -> Unit, – коллбэк для очистки ввода. Нужен, чтобы передавать событие клика за пределы функции, так как очищать будем теперь общие для экрана стейты.

Теперь задействуем новые параметры внутри функции.

  • email присваиваем value для визуального отображения.
  • Валидацию буду использовать в условии отображения состояния ошибки. Если почта не валидна !isEmailValid && email.*isNotBlank*(),. В label добавляю только проверку валидности. Вместо текста из errorState теперь показываем строку с текстом ошибки.
  • Коллбэк строки используем для отправки сигналов с текстом при вводе каждого символа. Вся логика будет вынесена, поэтому просто оставляем коллбэк и отправляем в него строку.
  • Коллбэк очистки текстового поля используем в одном единственном месте, где в функции иконки у нас очищаются стейты. Если вы задаетесь вопросом как где же корректнее хранить стейт – обратимся к все той же документации с рекомендациями. Вот что там говорится практически дословно:
Урок 10: Коллбэки (callback). Внедрение логики регистрации

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

Общее состояние экрана

Отлично, теперь функция CheckEmailField только принимает данные и кидает коллбэки, ничего не обрабатывает и не хранит. Что насчет нового хранения состояний?

Перенесем и немного модифицируем наши стейты для строки и ошибки в самое начало файла. Переименую их и, как уже упоминал ранее, стейт для результата валидации будет Boolean типа.

Однако, вспомним наше гипотетическое ТЗ: мы должны отображать результирующий текст. Там будут такие строки, как “регистрация пройдена”, “некорректный имейл”, “почта уже существует”. И вот для хранения этого состояния (по сути состояния функции Text) мы создадим еще один стейт. Он будет меняться динамически в зависимости от условий и содержания других стейтов. Сейчас все будет понятно наглядно.

Новый стейт для хранения сообщения с результатами валидации так и будет называться – validationMessage. Получается такой набор стейтов для текущего функционала экрана.

var userEmail by remember { mutableStateOf("") }
var isEmailFormatValid by remember { mutableStateOf(true) }
var validationMessage by remember { mutableStateOf("") }

Реализация логики CheckEmailField

Мы на финишной прямой. Стейты определены, функции подготовлены. Заполняем параметры CheckEmailField.

  • В параметр почты отправляем стейт, который будет хранить строку с почтой.
  • В параметр с флагом валидации аналогично.
  • Далее в лямбде с изменениями вэлью проводим схожие со старой логикой действия, а именно при каждом изменении события (ввода символа) присваиваем строку стейту со строкой. Далее проводим валидацию этой строки и записываем результат в соответствующий стейт. Тут же мы будем инициализировать в первый раз validationMessage. Если стейт с валидацией хранит false – будем записывать предупреждающее сообщение. Которое ранее мы показывали в label. В ином случае записывается пустая строка.
    • Наконец, последний параметр функции – onClearClicked. Этот коллбэк вызывает очистку всех текстовых стейтов, а стейта с валидацией – в исходное состояние true.

Реализация логики PrimaryButton

Промежуточно проверим что у нас получается. Все отрабатывает корректно, но кнопка все еще никаких действий не совершает, так как onRegisterClick пустой. Давайте наполним и его.

Итак, по сути нам нужно только брать введенные данные, сравнивать их между собой и заполнять тем или иным текстом стейт validationMessage. Сообщение из этого стейта будем отображать под кнопкой в качестве результатов проверки. Тут будет небольшая цепочка из условий.

  • Если почта пустая или невалидна – выводим "Некорректный email".
  • Также если почта совпадает с некой тестовой почтой – выводим "Такая почта уже существует". Добавлю какую-нибудь отладочную почту в начало документа в отдельную переменную.
  • Иначе выводим "Регистрация успешно пройдена".
PrimaryButton(
    text = "Зарегистрироваться",
    onRegisterClick = { it: String ->
        validationMessage =
            if (userEmail.isEmpty() || !isEmailFormatValid) {
                "Некорректный email"
            } else if (userEmail == testEmail) {
                "Такая почта уже существует"
            } else {
                "Регистрация успешно пройдена"
            }

        Log.i("!!!", "PrimaryButton: нажата кнопка $it")
    }
)

Отображение результатов регистрации

Наконец, остается одна скромная функция с выводом текста. Напишем ее прямо здесь.

Собственно добавляю отступ, добавляю функцию с текстом из validationMessage. Стиль возьму один из подготовленных заранее. Можно добавить отображение сообщение разным цветом, для этого буду проверять на наличие слова “успешно” в строке. Плохой подход, так как строки дело ненадежное ввиду их непостоянности, локализации и всего такого. Нужен в идеале отдельное состояние и для хранения цвета, либо привязывать его в другим стейтам успешной или неудачной регистрации.

Нам не всегда нужно показывать текст, а только по определенным условиям. Поэтому можно обернуть функцию в это условие, или добавить модификатор, который управляет прозрачностью. Второй вариант мне нравится больше, так как компонент будет оставаться физически на экране и мы попробуем новый экстеншн модификатора.

Этим модификатором является alpha. Принимает число с плавающей точкой, обозначающее прозрачность. Значения 0 и 1 дают полную прозрачность и непрозрачность соответственно.

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)
)

Это был финальный штрих для нашего скромного экрана с демонстрационной регистрацией. Можно тестировать, экспериментировать и интегрировать с логикой остального приложения, которое вы пишете.

Бесплатные Telegram-боты для обучения

Практика с проверкой кода и помощью ИИ-ментора

AndroidSprint AI Mentor

Проверяет Pull Request'ы в GitHub, проводит тестовые собеседования с голосом и таймером, помогает разбираться с кодом 24/7

Попробовать ИИ-ментора →

KotlinSprint Bot

22 урока Kotlin, 220 тестов, 120 практических задач с код-ревью

Начать обучение Kotlin →

AndroidSprint Bot

Тесты по Android SDK, Jetpack Compose, архитектуре приложений

Тесты по Android →

Тебе также может быть интересно

Узнать подробнее
Курс AndroidSprint

Глубокое обучение Android разработке с 0 до получения оффера. Только персональная практика с гарантией получения продуктового опыта.

Курс AndroidSprint - Глубокое <strong>обучение Android разработке с 0 до получения оффера</strong>. Только персональная практика с гарантией получения продуктового опыта.
Узнать подробнее
Узнать подробнее
Практикум по Kotlin

Изучение Котлин с 0 для профессиональной разработки. Личный ментор и разбор кода задач через git-flow.

Практикум по Kotlin - Изучение Котлин <strong>с 0 для профессиональной разработки</strong>. Личный ментор и разбор кода задач через git-flow.
Узнать подробнее
Узнать подробнее
Бесплатные уроки по Kotlin разработке

Самостоятельное освоение базы по языку для дальнейшего развития в Android/back-end разработке или в автотестах.

Бесплатные уроки по Kotlin разработке - <span>Самостоятельное освоение базы по языку для дальнейшего развития в Android/back-end разработке или в автотестах.</span>
Узнать подробнее
Узнать подробнее
Onboarding в разработку

Полное обучение Android разработке с нуля до получения оффера. Делаем упор на практику и обратную связь

Onboarding в разработку - <span>Полное обучение Android разработке с нуля до получения оффера. Делаем упор на практику и обратную связь</span>
Узнать подробнее
Узнать подробнее
Обучающий Kotlin телеграм бот (с тестами)

Ваш основной инструмент для изучения основ языка. Бесплатные тесты и практика внутри.

Обучающий Kotlin телеграм бот (с тестами) - Ваш основной <span>инструмент для изучения основ языка.</span> Бесплатные тесты и практика внутри.
Узнать подробнее
Узнать подробнее
Бесплатные уроки по Android разработке

Самостоятельное обучение разработке Андроид приложений. Понятные видеоуроки с разжеванными примерами.

Бесплатные уроки  по Android разработке - Самостоятельное <span>обучение разработке Андроид приложений.</span> Понятные видеоуроки с разжеванными примерами.
Узнать подробнее
Узнать подробнее
Курс по UI/Unit тестированию

Для ручных тестировщиков, которые готовы осваивать автотесты с использованием актуального стека технологий. [в разработке]

Курс по UI/Unit тестированию - Для ручных тестировщиков, которые <span>готовы осваивать автотесты</span> с использованием актуального стека технологий. [в разработке]
Узнать подробнее
Узнать подробнее
Обучающий Android телеграм бот (с тестами)

Бесплатные теоретические тесты для самопроверки. А также информер на практических спринтах по Android.

Обучающий Android телеграм бот (с тестами) - Бесплатные <span>теоретические тесты для самопроверки.</span> А также информер на практических спринтах по Android.
Узнать подробнее