I O Architecture Network Request Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource
I O Architecture Network Request Domain Interactors, Use case Interfaces Threading Data Repository Remote DataSource Local DataSource Presentation States View ViewModel
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
I O Architecture Network Request Scenario Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource
I O Architecture Domain Interactors, Use case Interfaces Threading Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Success Success Network Request Scenario
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
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
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?
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
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
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())
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
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
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
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
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
I O val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) .build() Modeling responses Retrofit CallAdapter
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
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.
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
I O Other Solutions Arrow - Typed Functional Programming suspend operator fun invoke(): Either<Throwable, List<Poster>> { return Either.catch { posterService.fetchPosters() }.mapLeft { it } }