ANDR-87: Логика экрана логина#145
Conversation
* ANDR-95: вынос общих полей ввода и логики валидации в core модули * ANDR-95: Обновил core/ui/build.gradle.kts: Добавил implementation(libs.androidx.icons) * ANDR-95: улучшена функция для максимальной переиспользуемости. Так же улучшена Preview часть * ANDR-95: переименовал функции понятным для всех неймингом, исправил ошибки ktlint * ANDR-95: рефакторинг TextField + реализация SearchTextField * ANDR-95: удаление старого после рефакторинга
#134) * ANDR-95-fix-text-field: добавил второй modifier для OutlinedTextField. Обновил шрифты и отступы согласно дизайну в фигме * ANDR-95-fix-text-field: добавилен параметр по умолчанию - readOnly: Boolean = false, и проброшен в остальные функции
- State / Action / Command / ViewModel - Sealed-модель состояний - Обработка команд вынесена
| data class LoginFieldErrorState( | ||
| val message: TextOrResource, | ||
| ) No newline at end of file |
There was a problem hiding this comment.
В текущей реализации выглядит как лишняя обёртка над TextOrResource.
Возможно понадобится, если появится необходимость в доп. полях в этом классе, например источник ошибки, но сейчас как будто не нужна.
| // 1. БАЗА (дизайн YeaHub) | ||
|
|
||
| @Composable | ||
| internal fun CoreTextField( | ||
| value: String, |
There was a problem hiding this comment.
Стоит замержить develop в epic/ADNR-81 и эпик в feature/ANDR-87, чтобы не было чужих изменений.
(возможно я не прав в конкретных шагах по устранению, но этих изменений точно не должно быть в ПРе)
| sealed interface LoginState { | ||
| val formState: LoginFormState | ||
|
|
||
| data class Initial( | ||
| override val formState: LoginFormState, | ||
| ) : LoginState | ||
|
|
||
| data class Editing( | ||
| override val formState: LoginFormState, | ||
| ) : LoginState | ||
|
|
||
| data class Validation( | ||
| override val formState: LoginFormState, | ||
| ) : LoginState | ||
|
|
||
| data class Loading( | ||
| override val formState: LoginFormState, | ||
| ) : LoginState | ||
|
|
||
| data class ServerError( | ||
| override val formState: LoginFormState, | ||
| ) : LoginState | ||
| } No newline at end of file |
There was a problem hiding this comment.
Мне кажется, State получился сложнее, чем нужно экрану. Классы-наследники не являются разными состояниями UI: во всех этих случаях экран рисует одну и ту же форму, а отличаются только значения полей emailError / passwordError / isSubmitEnabled.
Обычно наследников sealed state имеет смысл делать для заметно разных состояний экрана: условно Loaded - основной экран, Loading - когда показываем отдельную загрузку/скелетон будущего экрана(как на скрине), Error - когда показываем полноэкранную ошибку, например если не загрузились важные начальные данные для Loaded.
Если экрану логина не нужны отдельные Loading и Error экраны, то sealed class тут, кажется, не дает пользы. Можно оставить обычный data class LoginState или один Loaded, если хочется сохранить общий паттерн. Из-за текущего разбиения усложняются ViewModel и mapper: приходится гонять одни и те же поля между похожими состояниями.
Текущий Loading тоже немного вводит в заблуждение: он не переключает экран на отдельный loader/skeleton, а только показывает spinner в кнопке после клика по логину. Поэтому это скорее поле внутри LoginFormState, например isButtonLoading / isSubmitting : Boolean, которое можно менять так же, как остальные поля.
Если упростить state до одной модели формы, то большая часть when/copy-логики во ViewModel и mapper-е тоже уйдет сама собой.
| } | ||
| } | ||
|
|
||
| private fun getLocalPasswordError(password: String): TextOrResource? { |
There was a problem hiding this comment.
Метод возвращает ошибку, если пароль пуст, даже при первом запуске экрана. Кнопка должна быть заблокирована, но поле не должно подсвечиваться красным, пока пользователь не начал вводить данные
| password = rawState.password, | ||
| ) | ||
|
|
||
| val formState = when (rawState) { |
There was a problem hiding this comment.
Сейчас валидация срабатывает только в состоянии Validation. Если пользователь ввел некорректный email и убрал фокус (или просто перестал печатать), он не увидит ошибку, пока не нажмет кнопку Войти. У меня на экране регистрации валидация происходит когда поле теряет фокус
| command = LoginCommand.NavigateToMain, | ||
| ) | ||
| } catch (exception: LoginException) { | ||
| rawState.value = rawState.value.toValidationState() |
There was a problem hiding this comment.
Если у нас включен показ ошибок в этом состоянии, то при ошибке сети пользователь внезапно может увидеть и ошибки валидации полей, что его запутает. Лучше оставаться в текущем состоянии ввода, просто показав Snackbar
|
|
||
| private fun getLocalEmailError(email: String): TextOrResource? { | ||
| if (email.isEmpty()) { | ||
| return null |
There was a problem hiding this comment.
В маппере ошибка: если Email пустой, localEmailError возвращает null. Это приводит к тому, что кнопка сабмита становится активной даже при незаполненном поле почты
🧩 Что сделано:
Реализована логика экрана авторизации в рамках подхода State / Action / Command:
• Добавлены State, Action, Command
• Реализована ViewModel
• Введена sealed-модель состояний (raw state → UI state через ScreenMapper)
• Вынесена обработка одноразовых событий в Command
Работа с UI:
• UI подключен к состоянию и действиям пользователя
• Реализованы состояния кнопки: Disabled / Enabled / Loading
• Добавлено отображение ошибок:
• локальные (под полями)
• глобальные (Snackbar)
Валидация:
• Email — базовая проверка формата
• Password — проверка на непустое значение
Дополнительно:
• Добавлены Static и Dynamic Preview для основных состояний экрана
🗂 Затронутые модули:
• feature:authentication:impl