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
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
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
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
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
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
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
G E T S T R E A M . I O interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): List } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): List = try { posterService.fetchPosters() } catch (e: HttpException) { // error handling emptyList() } catch (e: Throwable) { // error handling emptyList() } } Retrofit API calls with Coroutines
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 } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): List = 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?
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)
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
G E T S T R E A M . I O Modeling responses Sealed Class sealed class NetworkResult { class Success(val data: T) : NetworkResult() class Error(val code: Int, val message: String?) : NetworkResult() class Exception(val e: Throwable) : NetworkResult() } ● NetowrkResult.Success: Success from the network request. ● NetowrkResult.Error: Failed from the network request. ● NetowrkResult.Exception: Unexpected exception. i.e. IOException, UnknownHostException
G E T S T R E A M . I O Modeling responses Sealed Interfaces sealed interface ApiResult class ApiSuccess(val data: T) : ApiResult class ApiError(val code: Int, val message: String?) : ApiResult class ApiException(val e: Throwable) : ApiResult ● ApiSuccess: Success from the network request. ● ApiError: Failed from the network request. ● ApiException: Unexpected exception. i.e. IOException, UnknownHostException
G E T S T R E A M . I O Modeling responses Sealed Interfaces sealed interface ApiResult sealed class ApiError(val code: Int, val message: String?) : ApiResult class BadRequest(val response: Response): ApiError(response.code(), response.message()) class Unauthorized(val response: Response): ApiError(response.code(), response.message()) class Forbidden(val response: Response): ApiError(response.code(), response.message())
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 sealed class ApiError(val code: Int, val message: String?) : ApiResult class BadRequest(val response: Response): ApiError(response.code(), response.message()) class Unauthorized(val response: Response): ApiError(response.code(), response.message()) class Forbidden(val response: Response): ApiError(response.code(), response.message())
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
G E T S T R E A M . I O Modeling responses Handling Retrofit responses suspend fun handleApi( execute: suspend () -> Response ): NetworkResult { 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) } }
G E T S T R E A M . I O Modeling responses suspend fun handleApi( execute: suspend () -> Response ): NetworkResult { 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
G E T S T R E A M . I O Modeling responses suspend fun handleApi( execute: suspend () -> Response ): NetworkResult { 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
G E T S T R E A M . I O Modeling responses suspend fun handleApi( execute: suspend () -> Response ): NetworkResult { 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
G E T S T R E A M . I O Modeling responses suspend fun handleApi( execute: suspend () -> Response ): NetworkResult { 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
G E T S T R E A M . I O interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): Response> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult> = handleApi { posterService.fetchPosters() } } Modeling responses Data Layer
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
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
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = NetworkResult.Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = NetworkResult.Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCall( private val proxy: Call ) : Call> { override fun enqueue(callback: Callback>) { proxy.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val networkResult = handleApi { response } callback.onResponse([email protected], Response.success(networkResult)) } override fun onFailure(call: Call, t: Throwable) { val networkResult = NetowrkResult.Exception(t) callback.onResponse([email protected], Response.success(networkResult)) } }) } Modeling responses Custom Retrofit Call override fun execute(): Response> = throw NotImplementedError() override fun clone(): Call> = 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() }
G E T S T R E A M . I O class NetworkResultCallAdapter( private val resultType: Type ) : CallAdapter>> { override fun responseType(): Type = resultType override fun adapt(call: Call): Call> { return NetworkResultCall(call) } } Modeling responses Retrofit CallAdapter
G E T S T R E A M . I O class NetworkResultCallAdapter( private val resultType: Type ) : CallAdapter>> { override fun responseType(): Type = resultType override fun adapt(call: Call): Call> { return NetworkResultCall(call) } } Modeling responses Retrofit CallAdapter
G E T S T R E A M . I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, 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
G E T S T R E A M . I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, 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
G E T S T R E A M . I O class NetworkResultCallAdapterFactory private constructor() : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, 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
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
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>> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult> { return posterService.fetchPosters().await() } }
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> } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): NetworkResult> { return posterService.fetchPosters() } }
G E T S T R E A M . I O Modeling responses Extensions suspend fun NetworkResult.onSuccess( executable: suspend (T) → Unit ): NetworkResult = apply { if (this is NetworkResult.Success) { executable(data) } } suspend fun NetworkResult.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Error) { executable(code, message) } } suspend fun NetworkResult.onException( executable: suspend (e: Throwable) → Unit ): NetworkResult = apply { if (this is NetworkResult.Exception) { executable(e) } }
G E T S T R E A M . I O Modeling responses Extensions suspend fun NetworkResult.onSuccess( executable: suspend (T) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Success) { executable(data) } } suspend fun NetworkResult.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Error) { executable(code, message) } } suspend fun NetworkResult.onException( executable: suspend (e: Throwable) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Exception) { executable(e) } }
G E T S T R E A M . I O Modeling responses Extensions suspend fun NetworkResult.onSuccess( executable: suspend (T) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Success) { executable(data) } } suspend fun NetworkResult.onError( executable: suspend (code: Int, message: String?) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Error) { executable(code, message) } } suspend fun NetworkResult.onException( executable: suspend (e: Throwable) -> Unit ): NetworkResult = apply { if (this is NetworkResult.Exception) { executable(e) } }
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}") } }
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 NetworkResult.map( transformer: (value: T) -> R ): NetworkResult { return if (this is NetworkResult.Success) { NetworkResult.Success(transformer(data)) } else { this as NetworkResult } }
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 NetworkResult.map( transformer: (value: T) -> R ): NetworkResult { return if (this is NetworkResult.Success) { NetworkResult.Success(transformer(data)) } else { this as NetworkResult } }
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
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.
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
G E T S T R E A M . I O Other Solutions interface PosterService { @GET("DisneyPosters.json") suspend fun fetchPosters(): List } class PosterRemoteDataSource( private val posterService: PosterService ) { suspend operator fun invoke(): Result> { return kotlin.runCatching { posterService.fetchPosters() } } } Kotlin Result
G E T S T R E A M . I O Other Solutions Arrow - Typed Functional Programming suspend operator fun invoke(): Either> { return Either.catch { posterService.fetchPosters() }.mapLeft { it } }
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