Каждый, кто использовал Чистую архитектуру в разработке, сталкивался с проблемой передачи данных между слоями. Суть проблемы всегда одинакова: необходимо вернуть либо результат, либо ошибку. Представить это можно, например, так:
interface Reactiondata class Success(val data: String) : Reactiondata 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: Repositoryfun doSomething() {viewModelScope.launch(Dispatchers.IO) {val result = repository.getData()when (result) {is Success -> //do somethingis 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: Repositoryprivate val _onData = MutableLiveData<State>()val onData: LiveData<State> = _onDatafun 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: Repositoryprivate val _onData = MutableLiveData<State>()val onData: LiveData<State> = _onNewDirectoryfun 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 = 0val 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)
Преимущества:-
Является почти полной копией предлагаемого мной решения
Недостатки:
-
Можно включить использование в gradle, но на свой страх и риск, потому что Result, является частью языка Kotlin и может поменять поведение
-
Итог
Reaction - это легковесная библиотека с минимальным порогом вхождения, т.к. она состоит из 1 файла, предоставляющая такие же мощности, как решение от Kotlin, но не содержит всех его минусов.