Reaction — обработка результатов методов в Kotlin

от автора

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

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, но не содержит всех его минусов.

GitHub

https://github.com/taptappub/Reaction/

ссылка на оригинал статьи https://habr.com/ru/post/545926/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *