[Open source, Разработка под Android, Kotlin] Reaction — обработка результатов методов в Kotlin

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
07-Мар-2021 22:30


Каждый, кто использовал Чистую архитектуру в разработке, сталкивался с проблемой передачи данных между слоями. Суть проблемы всегда одинакова: необходимо вернуть либо результат, либо ошибку. Представить это можно, например, так:
interface Reaction
data class Success(val data: String) : Reaction
data class Error(message: String) : Reaction
В зависимости от задачи, такие Reaction’ы могут быть самые разные, поэтому давайте объединим его в один класс, используя Genericsи Sealed class’ы.
sealed class Reaction<out T> {
   class Success<out T>(val data: T) : Reaction<T>()
   class Error(val exception: Throwable) : Reaction<Nothing>()
}
Разберем пример как это можно использовать
class MyViewModel : ViewModel {
  private val repository: Repository
  fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
      val result = repository.getData()
      when (result) {
        is Success -> //do something
        is Error -> // show error
      }
    }
  }
}
Выглядит неплохо. Мы можем возвращать данные и обрабатывать ошибку.
Теперь посмотрим как выглядит репозиторий в текущем варианте
class RepositoryImpl(private val dataSource: DataSource) : Repository {
  override suspend fun getData(): Reaction<Int> {
    return try {
      Reaction.Success(dataSource.data)
    } catch(e: Exception) {
      Reaction.Error(e)
    }
  }
}
Из-за того, что каждый метод репозитория должен возвращать Reaction, придется каждый метод оборачивать в try-catch, что выглядит некрасиво из-за огромного количества бойлерплейт кода. Попробуем сделать код чище, выносом try-catch в метод.
sealed class Reaction<out T> {
   class Success<out T>(val data: T) : Reaction<T>()
   class Error(val exception: Throwable) : Reaction<Nothing>()
   companion object {
       inline fun <T> on(f: () -> T): Reaction<T> = try {
           Success(f())
       } catch (ex: Exception) {
           Error(ex)
       }
   }
}
После этого репозиторий начнет выглядеть так:
class RepositoryImpl(private val dataSource: DataSource) : Repository {
  suspend fun getData(): Reaction<Int> = Reaction.on { dataSource.data }
}
Видно, что код стал гораздо чище и только в этом примере мы сэкономили 4 строки кода.Теперь вернемся к ViewModelи постараемся убрать бойлерплэйт when для каждого запроса. Сейчас мы получаем данные, обрабатываем и отдаем во View.
class MyViewModel : ViewModel {
  private val repository: Repository
  private val _onData = MutableLiveData<State>()
  val onData: LiveData<State> = _onData
  fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
      val result = repository.getData()
      when (result) {
        is Success -> _onData.postValue(State.Success)
        is Error -> onData.postValue(State.Error(result.message))
      }
    }
  }
  sealed class State {
    object Progress : State()
    object Success : State()
    data class Error(message: String) : State()
  }
}
Решение уже подсказывает опыт RxJava, Coroutinesи LiveData.
Исходя из того, что данные, которые вернулись в ViewModelобычно надо показать пользователю в виде результата запроса, либо ошибки, давайте добавим метод zip, который будет приводить Reaction к объекту, который будет передаваться в LiveData
inline fun <T, R> Result<T>.zip(success: (T) -> R, error: (Exception) -> R): R =
   when (this) {
       is Reaction.Success -> success(this.data)
       is Reaction.Error -> error(this.exception)
   }
Наша MyViewModel преобразится в
class MyViewModel : ViewModel {
  private val repository: Repository
  private val _onData = MutableLiveData<State>()
  val onData: LiveData<State> = _onNewDirectory
  fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
      repository.getData()
        .zip(
          { State.Success },
          { State.Error(result.message) }
        )
        .let { onData.postValue(it) }
    }
  }
  //...
}
Также есть частый случай, когда метод в ViewModel делает несколько последовательных запросов к репозиторию, где одни данные зависят от ранее полученных. При получении ошибки необходимо прервать цепочку запросов и вернуть ошибку во ViewРассмотрим следующий пример:
class MyViewModel : ViewModel {
  //...
  fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
      var firstData: Int = 0
      val reaction = repository.getData()
      when (reaction) {
        is Success -> firstData = reaction.data
        is Error -> {
          onData.postValue(State.Error(reaction.message))
          return@launch
        }
      }
      val nextReaction = repository.getNextData(firstData)
      //..
    }
  }
  //...
}
Решений можно придумать множество, но я здесь представлю решение без callback hell, оставляя преимущество, которое предоставляет использование Coroutines
class MyViewModel : ViewModel {
  //...
  fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
      val firstData = repository.getData()
        .takeOrReturn {
          onData.postValue(State.Error(result.message)
          return@launch
        }
      val nextReaction= repository.getNextData(firstData)
      //..
    }
  }
}
В итоге мы имеем легко расширяемое решение, в которой уже есть такие популярные методы обработки полученных данных как:
  • on - Создает Reaction из выражения
  • map - Трансформирует успешный результат
  • flatMap - Трансформирует успешный результат в новую Reaction
  • doOnSuccess - Выполняется, если Reaction - успешный результат
  • и др
Полный список и дополнительные примеры можно найти в GithubСравнение с аналогамиБыло найдено 3 аналога. Ниже представлены сами аналоги и их преимущества и недостатки
  • Railway Kotlin
    Преимущества:
    • Легко освоить
    • Состоит из 1 файла
    Недостатки:
    • Нет возможности инкапсулировать try-catch
    • Использование infix методов
    • Неинтуитивные названия методов
  • Arrow-KT
    Преимущества:
    • Популярная библиотека
    Недостатки:
    • Из описания непонятно что библиотека может
    • Высокий порог вхождения по сравнению с аналогами
    • Оставляет ощущение, что является слишком сложной для решения такой простой проблемы
  • Result (Kotlin)
    Преимущества:
    • Является почти полной копией предлагаемого мной решения
    Недостатки:
ИтогReaction - это легковесная библиотека с минимальным порогом вхождения, т.к. она состоит из 1 файла, предоставляющая такие же мощности, как решение от Kotlin, но не содержит всех его минусов.GitHubhttps://github.com/taptappub/Reaction/
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_open_source, #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_kotlin_result, #_reaction, #_android, #_android_development, #_kotlin, #_open_source, #_razrabotka_pod_android (
Разработка под Android
)
, #_kotlin
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 15:14
Часовой пояс: UTC + 5