Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Modeling Retrofit responses with Sealed classes and Coroutines

Jaewoong
April 20, 2022

Modeling Retrofit responses with Sealed classes and Coroutines

Modeling Retrofit responses with Sealed classes and Coroutines at Android Worldwide.

Jaewoong

April 20, 2022
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. G E T S T R E A M .

    I O Modeling Retrofit Responses With Sealed Classes + Coroutines
  2. G E T S T R E A M .

    I O skydoves @github_skydoves Android Developer Advocate @ Stream Jaewoong Eum
  3. G E T S T R E A M .

    I O Architecture Network Request Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource
  4. G E T S T R E A M .

    I O Architecture Network Request Domain Interactors, Use case Interfaces Threading Data Repository Remote DataSource Local DataSource Presentation States View ViewModel
  5. G E T S T R E A M .

    I O Architecture Network Request Data Repository Remote DataSource Local DataSource Response Success - body - headers - status code Failure - error body - headers - status code Exception - IOException - UnKnownHostException - SSLHandshakeException - … Error
  6. G E T S T R E A M .

    I O Architecture Network Request Scenario Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource
  7. G E T S T R E A M .

    I O Architecture Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Success Success Network Request Scenario
  8. G E T S T R E A M .

    I O Architecture Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Error Error Success Success Network Request Scenario
  9. G E T S T R E A M .

    I O Architecture Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Error Error Exception? Exception? Exception? Success Success Network Request Scenario
  10. G E T S T R E A M .

    I O interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): List<Poster> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): List<Poster> = try { posterService.fetchPosters() } catch (e: HttpException) { // error handling emptyList() } catch (e: Throwable) { // error handling emptyList() } } Retrofit API calls with Coroutines
  11. G E T S T R E A M .

    I O Retrofit API calls with Coroutines interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): List<Poster> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): List<Poster> = try { posterService.fetchPosters() } catch (e: HttpException) { // error handling emptyList() } catch (e: Throwable) { // error handling emptyList() } } Problem • Results are ambiguous to callers • Callers don't know the exception types val data = posterRemoteDataSource.invoke() if (data.isNotEmpty()) { ... } Success? or Failure?
  12. G E T S T R E A M .

    I O Architecture Sealed Classes Data Repository Remote DataSource Local DataSource Response Success - body - headers - status code Failure - error body - headers - status code Exception - IOException - UnKnownHostException - SSLHandshakeException - … Error Sealed Classes (=Result)
  13. G E T S T R E A M .

    I O Architecture Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Error Error try-catch Success Success Network Request Scenario
  14. G E T S T R E A M .

    I O Modeling responses Sealed Class sealed class NetworkResult<T : Any> { class Success<T: Any>(val data: T) : NetworkResult<T>() class Error<T: Any>(val code: Int, val message: String?) : NetworkResult<T>() class Exception<T: Any>(val e: Throwable) : NetworkResult<T>() } • NetowrkResult.Success: Success from the network request. • NetowrkResult.Error: Failed from the network request. • NetowrkResult.Exception: Unexpected exception. i.e. IOException, UnknownHostException
  15. G E T S T R E A M .

    I O Modeling responses Sealed Interfaces sealed interface ApiResult<T : Any> class ApiSuccess<T : Any>(val data: T) : ApiResult<T> class ApiError<T : Any>(val code: Int, val message: String?) : ApiResult<T> class ApiException<T : Any>(val e: Throwable) : ApiResult<T> • ApiSuccess: Success from the network request. • ApiError: Failed from the network request. • ApiException: Unexpected exception. i.e. IOException, UnknownHostException
  16. G E T S T R E A M .

    I O Modeling responses Sealed Interfaces sealed interface ApiResult<T : Any> sealed class ApiError<T : Any>(val code: Int, val message: String?) : ApiResult<T> class BadRequest<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message()) class Unauthorized<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message()) class Forbidden<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message())
  17. G E T S T R E A M .

    I O Modeling responses Sealed Interfaces when (val response = posterRemoteDataSource.invoke()) { is ApiError → { when (response) { is ApiError.BadRequest → Unit is ApiError.Unauthorized → Unit is ApiError.Forbidden → Unit } } sealed interface ApiResult<T : Any> sealed class ApiError<T : Any>(val code: Int, val message: String?) : ApiResult<T> class BadRequest<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message()) class Unauthorized<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message()) class Forbidden<T :Any>(val response: Response<T>): ApiError<T>(response.code(), response.message())
  18. G E T S T R E A M .

    I O Modeling responses Sealed Classes vs Sealed Interfaces Sealed Classes Sealed Interfaces Declaration restrictions Same file Same module Inheritance limitations Single parent class Multiple sealed hierarchies API surfaces Selectively public Must be public
  19. G E T S T R E A M .

    I O Modeling responses Handling Retrofit responses Retrofit Response NetworkResult<T> handleApi
  20. G E T S T R E A M .

    I O Modeling responses Handling Retrofit responses suspend fun <T : Any> handleApi( execute: suspend () -> Response<T> ): NetworkResult<T> { return try { val response = execute() val body = response.body() if (response.isSuccessful && body != null) { NetworkResult.Success(body) } else { NetworkResult.Error(code = response.code(), message = response.message()) } } catch (e: HttpException) { NetworkResult.Error(code = e.code(), message = e.message()) } catch (e: Throwable) { NetworkResult.Exception(e) } }
  21. G E T S T R E A M .

    I O Modeling responses suspend fun <T : Any> handleApi( execute: suspend () -> Response<T> ): NetworkResult<T> { return try { val response = execute() val body = response.body() if (response.isSuccessful && body != null) { NetworkResult.Success(body) } else { NetworkResult.Error(code = response.code(), message = response.message()) } } catch (e: HttpException) { NetworkResult.Error(code = e.code(), message = e.message()) } catch (e: Throwable) { NetworkResult.Exception(e) } } Handling Retrofit responses
  22. G E T S T R E A M .

    I O Modeling responses suspend fun <T : Any> handleApi( execute: suspend () -> Response<T> ): NetworkResult<T> { return try { val response = execute() val body = response.body() if (response.isSuccessful && body != null) { NetworkResult.Success(body) } else { NetworkResult.Error(code = response.code(), message = response.message()) } } catch (e: HttpException) { NetworkResult.Error(code = e.code(), message = e.message()) } catch (e: Throwable) { NetworkResult.Exception(e) } } Handling Retrofit responses
  23. G E T S T R E A M .

    I O Modeling responses suspend fun <T : Any> handleApi( execute: suspend () -> Response<T> ): NetworkResult<T> { return try { val response = execute() val body = response.body() if (response.isSuccessful && body != null) { NetworkResult.Success(body) } else { NetworkResult.Error(code = response.code(), message = response.message()) } } catch (e: HttpException) { NetworkResult.Error(code = e.code(), message = e.message()) } catch (e: Throwable) { NetworkResult.Exception(e) } } Handling Retrofit responses
  24. G E T S T R E A M .

    I O Modeling responses suspend fun <T : Any> handleApi( execute: suspend () -> Response<T> ): NetworkResult<T> { return try { val response = execute() val body = response.body() if (response.isSuccessful && body != null) { NetworkResult.Success(body) } else { NetworkResult.Error(code = response.code(), message = response.message()) } } catch (e: HttpException) { NetworkResult.Error(code = e.code(), message = e.message()) } catch (e: Throwable) { NetworkResult.Exception(e) } } Handling Retrofit responses
  25. G E T S T R E A M .

    I O interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): Response<List<Poster>> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult<List<Poster>> = handleApi { posterService.fetchPosters() } } Modeling responses Data Layer
  26. G E T S T R E A M .

    I O viewModelScope.launch { when (val response = posterRemoteDataSource.invoke()) { is NetworkResult.Success → posterFlow.emit(response.data) is NetworkResult.Error → errorFlow.emit("${response.code} ${response.message}") is NetworkResult.Exception → errorFlow.emit("${response.e.message}") } } Modeling responses ViewModel
  27. G E T S T R E A M .

    I O viewModelScope.launch { when (val response = posterRemoteDataSource.invoke()) { is NetworkResult.Success → posterFlow.emit(response.data) is NetworkResult.Error → errorFlow.emit( when (response.code) { 400 → "BadRequest" 401 → "Unauthorized" else → "Unknown" } ) is NetworkResult.Exception → errorFlow.emit(response.e.message) } } Modeling responses ViewModel
  28. G E T S T R E A M .

    I O Architecture API data flow Presentation Repository Retrofit Domain
  29. G E T S T R E A M .

    I O Architecture API data flow Domain Presentation Repository Retrofit handleApi handleApi handleApi
  30. G E T S T R E A M .

    I O Architecture API data flow Domain Presentation Repository Retrofit handleApi handleApi handleApi
  31. G E T S T R E A M .

    I O Retrofit Retrofit CallAdapter Modeling responses Retrofit CallAdapter Response Exception Response Type
  32. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  33. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  34. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  35. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  36. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = NetworkResult.Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  37. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = NetworkResult.Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  38. G E T S T R E A M .

    I O class NetworkResultCall<T : Any>( private val proxy: Call<T> ) : Call<NetworkResult<T>> { override fun enqueue(callback: Callback<NetworkResult<T>>) { proxy.enqueue(object : Callback<T> { override fun onResponse(call: Call<T>, response: Response<T>) { val networkResult = handleApi { response } callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } override fun onFailure(call: Call<T>, t: Throwable) { val networkResult = NetowrkResult.Exception<T>(t) callback.onResponse(this@NetworkResultCall, Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError() override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone()) override fun request(): Request = proxy.request() override fun timeout(): Timeout = proxy.timeout() override fun isExecuted(): Boolean = proxy.isExecuted override fun isCanceled(): Boolean = proxy.isCanceled override fun cancel() { proxy.cancel() }
  39. G E T S T R E A M .

    I O class NetworkResultCallAdapter( private val resultType: Type ) : CallAdapter<Type, Call<NetworkResult<Type>>> { override fun responseType(): Type = resultType override fun adapt(call: Call<Type>): Call<NetworkResult<Type>> { return NetworkResultCall(call) } } Modeling responses Retrofit CallAdapter
  40. G E T S T R E A M .

    I O class NetworkResultCallAdapter( private val resultType: Type ) : CallAdapter<Type, Call<NetworkResult<Type>>> { override fun responseType(): Type = resultType override fun adapt(call: Call<Type>): Call<NetworkResult<Type>> { return NetworkResultCall(call) } } Modeling responses Retrofit CallAdapter
  41. G E T S T R E A M .

    I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { return null } val callType = getParameterUpperBound(0, returnType as ParameterizedType) if (getRawType(callType) != NetworkResult::class.java) { return null } val resultType = getParameterUpperBound(0, callType as ParameterizedType) return NetworkResultCallAdapter(resultType) } companion object { fun create(): NetworkResultCallAdapterFactory = NetworkResultCallAdapterFactory() } } Modeling responses Retrofit CallAdapter Factory
  42. G E T S T R E A M .

    I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { return null } val callType = getParameterUpperBound(0, returnType as ParameterizedType) if (getRawType(callType) != NetworkResult::class.java) { return null } val resultType = getParameterUpperBound(0, callType as ParameterizedType) return NetworkResultCallAdapter(resultType) } companion object { fun create(): NetworkResultCallAdapterFactory = NetworkResultCallAdapterFactory() } } Modeling responses Retrofit CallAdapter Factory
  43. G E T S T R E A M .

    I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { return null } val callType = getParameterUpperBound(0, returnType as ParameterizedType) if (getRawType(callType) != NetworkResult::class.java) { return null } val resultType = getParameterUpperBound(0, callType as ParameterizedType) return NetworkResultCallAdapter(resultType) } companion object { fun create(): NetworkResultCallAdapterFactory = NetworkResultCallAdapterFactory() } } Modeling responses Retrofit CallAdapter Factory
  44. G E T S T R E A M .

    I O val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) .build() Modeling responses Retrofit CallAdapter
  45. G E T S T R E A M .

    I O val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) .build() Modeling responses Retrofit CallAdapter interface PosterService { @GET("DisneyPosters.json") fun fetchPosters(): Call<NetworkResult<List<Poster>>> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult<List<Poster>> { return posterService.fetchPosters().await() } }
  46. G E T S T R E A M .

    I O val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) .build() Modeling responses Retrofit CallAdapter interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): NetworkResult<List<Poster>> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult<List<Poster>> { return posterService.fetchPosters() } }
  47. G E T S T R E A M .

    I O Domain Presentation Repository Retrofit Retrofit CallAdapter Modeling responses Retrofit CallAdapter
  48. G E T S T R E A M .

    I O Modeling responses Extensions NetworkResult<T> NetworkResult<R> Map Combine Transformer
  49. G E T S T R E A M .

    I O Modeling responses Extensions suspend fun <T : Any> NetworkResult<T>.onSuccess( executable: suspend (T) → Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Success<T>) { executable(data) } } suspend fun <T : Any> NetworkResult<T>.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Error<T>) { executable(code, message) } } suspend fun <T : Any> NetworkResult<T>.onException( executable: suspend (e: Throwable) → Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Exception<T>) { executable(e) } }
  50. G E T S T R E A M .

    I O Modeling responses Extensions suspend fun <T : Any> NetworkResult<T>.onSuccess( executable: suspend (T) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Success<T>) { executable(data) } } suspend fun <T : Any> NetworkResult<T>.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Error<T>) { executable(code, message) } } suspend fun <T : Any> NetworkResult<T>.onException( executable: suspend (e: Throwable) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Exception<T>) { executable(e) } }
  51. G E T S T R E A M .

    I O Modeling responses Extensions suspend fun <T : Any> NetworkResult<T>.onSuccess( executable: suspend (T) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Success<T>) { executable(data) } } suspend fun <T : Any> NetworkResult<T>.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Error<T>) { executable(code, message) } } suspend fun <T : Any> NetworkResult<T>.onException( executable: suspend (e: Throwable) -> Unit ): NetworkResult<T> = apply { if (this is NetworkResult.Exception<T>) { executable(e) } }
  52. G E T S T R E A M .

    I O Modeling responses Extensions viewModelScope.launch { val response = posterRemoteDataSource.invoke() response.onSuccess { posterList → posterFlow.emit(posterList) }.onError { code, message → errorFlow.emit("$code $message") }.onException { errorFlow.emit("${it.message}") } }
  53. G E T S T R E A M .

    I O Modeling responses Extensions viewModelScope.launch { val response = posterRemoteDataSource.invoke() response.onSuccess { posterList → posterFlow.emit(posterList) }.onError { code, message → errorFlow.emit("$code $message") }.onException { errorFlow.emit("${it.message}") } } suspend fun <T : Any, R : Any> NetworkResult<T>.map( transformer: (value: T) -> R ): NetworkResult<R> { return if (this is NetworkResult.Success<T>) { NetworkResult.Success(transformer(data)) } else { this as NetworkResult<R> } }
  54. G E T S T R E A M .

    I O Modeling responses Extensions viewModelScope.launch { val response = posterRemoteDataSource.invoke() response.map { posterList → posterList.first() }.onSuccess { poster → posterFlow.emit(poster) }.onError { code, message → errorFlow.emit("$code $message") }.onException { errorFlow.emit("${it.message}") } } suspend fun <T : Any, R : Any> NetworkResult<T>.map( transformer: (value: T) -> R ): NetworkResult<R> { return if (this is NetworkResult.Success<T>) { NetworkResult.Success(transformer(data)) } else { this as NetworkResult<R> } }
  55. G E T S T R E A M .

    I O Summarize • Encapsulate raw data/exception from a network result • Advanced error/exception handling • We can expect the return type and improve them with functional operators ◦ Monad ◦ Functional Programming ◦ Railway oriented programming • Clear behaviors in domain/presentation layers by propagating the result
  56. G E T S T R E A M .

    I O Sandwich Other Solutions • ApiResponse.Success: Success from the network request. • ApiResponse.Error: Failed from the network request. • ApiResponse.Exception: Unexpected exception. i.e. IOException, UnknownHostException.
  57. G E T S T R E A M .

    I O • ApiResponse.getOrNull • ApiResponse.getOrElse • ApiResponse.getOrThrow Retrieve body data • ApiSuccessModelMapper • ApiErrorModelMapper • map extensions Mapper • ApiResponseOperator • ApiResponseSuspendOperator • Sandwich Global Operator Operator • Merge extensions • ApiResponseMergePolicy Merge • ApiResponse.Success.statusCode • ApiResponse.Failure.Error.statusCode StatusCode • ApiResponse.toFlow() • ApiResponse.toLiveData() Interoperability Sandwich Other Solutions
  58. G E T S T R E A M .

    I O Other Solutions Kotlin Result
  59. G E T S T R E A M .

    I O Other Solutions interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): List<Poster> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): Result<List<Poster>> { return kotlin.runCatching { posterService.fetchPosters() } } } Kotlin Result
  60. G E T S T R E A M .

    I O Other Solutions Arrow - Typed Functional Programming suspend operator fun invoke(): Either<Throwable, List<Poster>> { return Either.catch { posterService.fetchPosters() }.mapLeft { it } }
  61. G E T S T R E A M .

    I O Resources • Retrofit ◦ https://github.com/square/retrofit ◦ https://square.github.io/retrofit/2.x/retrofit/retrofit2/CallAdapter.html • Kotlin Sealed classes/interfaces ◦ https://kotlinlang.org/docs/sealed-classes.html • Functional Programming ◦ https://en.wikipedia.org/wiki/Functional_programming • Sandwich library ◦ https://github.com/skydoves/sandwich • Kotlin Result ◦ https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/ ◦ https://github.com/Kotlin/KEEP/blob/master/proposals/stdlib/result.md • Arrow ◦ https://github.com/arrow-kt/arrow
  62. G E T S T R E A M .

    I O https://github.com/skydoves [email protected] https://twitter.com/github_skydoves https://medium.com/@skydoves Contact
  63. G E T S T R E A M .

    I O Thank you.