Реактивная обработка ошибок в архитектуре, управляемой MVVM

Итак, мы здесь, спустя несколько месяцев после того, как Google объявил о своих компонентах архитектуры, и хотя MVVM медленно внедряется и встраивается в наши любимые проекты Android, некоторые вопросы все еще витают в воздухе.

Один из таких вопросов: «Как обрабатывать ошибки?».

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

Эта проблема

В идеале мы хотели бы использовать вышеупомянутые конструкции LiveData / Rx. Однако, имея что-то похожее на это:

val emailEmptyError = MutableLiveData<Boolean>()
val emailInvalidFormatError = MutableLiveData<Boolean>()

val passwordEmptyError = MutableLiveData<Boolean>()
val passwordTooShortError = MutableLiveData<Boolean>()

это перебор. У нас было бы слишком много LiveDatas для наблюдения: один для пустого состояния, один для «слишком короткого» состояния (для имен пользователей / паролей) и один для недопустимого формата (электронные письма, имена пользователей и пароли, если им нужен хотя бы один специальный символ. ). И у нас должны быть все эти данные для каждого поля ввода.

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

Еще больше раздражает то, что нам придется повторно использовать логику для нескольких экранов (электронная почта в регистре и логин), что в целом является большим нет.

Итак, нам нужны LiveData состояния ошибки, но в какой форме? Вы только что ответили на вопрос сами себе! У нас будет состояние ошибки для каждого поля ввода.

Но как???

Позвольте познакомить вас с чем-то, что называется перечислениями!

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

Интересно, можем ли мы использовать это в наших интересах? (Подсказка: да)

Решение

Сначала мы объявим интерфейс, чтобы абстрагироваться от ошибок:

interface ErrorEvent {

  @StringRes
  fun getErrorResource(): Int
}

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

Затем мы определяем наши События. Мы возьмем пока только один, остальные следуют тому же принципу.

enum class EmailErrorEvent(@StringRes private val resourceId: Int) : ErrorEvent {
  NONE(0),
  EMPTY(R.string.email_empty_error),
  INVALID_FORMAT(R.string.email_format_error);

  override fun getErrorResource() = resourceId
}

Здесь мы можем определить множество случаев, в зависимости от требований нашего приложения. Ключевым моментом здесь является то, что вместо множества объектов LiveData ‹Boolean› в нашей viewModel мы можем иметь только один: LiveData ‹EmailErrorEvent›.

Как бы выглядела наша проверка сейчас?

when {
  //name validation
  ...

  email.isBlank() -> emailError.value = EmailErrorEvent.EMPTY
  !email.isEmail() -> emailError.value = EmailErrorEvent.INVALID_FORMAT
  //password validation
  ...
  else -> { //let's register the user
    val request = RegisterRequest(email, password, name)

    authInteractor.registerUser(request, getUserRegisterCallback())
  }
}

Наша проверка теперь очень проста и удобочитаема, и ее можно протестировать исключительно с помощью модульных тестов (путем утверждения правильных значений перечисления), и она очень открыта / закрыта, поскольку мы можем добавить новый кейс, ничего не нарушая. И все, что нужно сделать пользовательскому интерфейсу, это:

//an extension method to shorten our code
viewModel.emailError.subscribe(this, this::onEmailError)
private fun onEmailError(emailError: EmailErrorEvent) {
  email.error = getError(this, emailError)
}

Где глобальная функция getError (context, errorEvent) определяется как:

fun getError(from: Context, errorEvent: ErrorEvent) = if (errorEvent.getErrorResource() == 0) {
  null
} else {
  from.getString(errorEvent.getErrorResource())
}

Преимущества (поверьте, их несколько)

  1. Теперь мы абстрагировались от состояний ошибок за серией перечислений. Это позволяет нам грамотно использовать операторы when и объекты LiveData, чтобы быстро назначить текущую ошибку для отображения.
  2. Пользовательскому интерфейсу не нужно выполнять лишнюю работу, он просто передает строку с ошибкой в ​​соответствующее поле ввода.
  3. ErrorEvents можно многократно использовать, поэтому вы можете без проблем добавлять их на другие экраны проверки.
  4. Все сводится к чистым модульным тестам (более или менее), чтобы проверить, отображается ли правильная ошибка.
  5. Добавить новую ошибку так же просто, как добавить новый регистр перечисления.

Заключение

Не обращая внимания на различные варианты обработки ошибок, с которыми мне приходилось сталкиваться за время разработки, я понял, что это самый чистый и понятный для меня.

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

Однако, если у вас есть какие-либо другие идеи или улучшения для нашей текущей обработки ошибок, обязательно дайте мне знать! Лучше всего учиться, экспериментируя с разными вещами вместе. :)

Филип Бабич - разработчик Android в COBE и студент факультета информатики в FERIT, Осиек. Он большой поклонник Котлина и иногда проводит мини-мастерские и встречи Котлина в Осиеке. Ему нравится узнавать что-то новое, играть в DnD и писать о вещах, которые он любит больше всего. Когда он не занимается программированием, не пишет о кодировании, не учится программированию или не учит других программированию, он питает свою внутреннюю нервозность, играя и просматривая фантастические шоу.

Пока вы здесь, ознакомьтесь с другими статьями COBE: