Skip to content

Commit e6ee028

Browse files
Merge pull request #23 from CodandoTV/feature/22_tratamento_networking
Networking treatment using flow or Coroutines with Result
2 parents 0499035 + 2b5b654 commit e6ee028

File tree

15 files changed

+375
-15
lines changed

15 files changed

+375
-15
lines changed

core-networking/src/main/java/com/codandotv/streamplayerapp/core_networking/di/NetworkModule.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.codandotv.streamplayerapp.core_networking.di
22

33
import com.codandotv.streamplayerapp.core_networking.BuildConfig
4+
import com.codandotv.streamplayerapp.core_networking.handleError.coroutines.NetworkResponseAdapter
5+
import com.codandotv.streamplayerapp.core_networking.handleError.coroutines.NetworkResponseAdapterFactory
46
import com.squareup.moshi.Moshi
57
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
68
import okhttp3.Interceptor
@@ -84,5 +86,6 @@ object NetworkModule {
8486
.baseUrl(baseUrl)
8587
.client(okHttpClient)
8688
.addConverterFactory(MoshiConverterFactory.create(moshi))
89+
.addCallAdapterFactory(NetworkResponseAdapterFactory(moshi))
8790
.build()
8891
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError
2+
3+
import com.codandotv.streamplayerapp.core_networking.R
4+
import org.koin.core.component.KoinComponent
5+
6+
/**
7+
* Base Class for handling errors/failures/exceptions.
8+
*/
9+
sealed class Failure(
10+
val code: Int? = -1,
11+
val errorMessage: String? = null,
12+
val errorMessageRes: Int = R.string.core_networking_msg_default_error
13+
) : Exception(), KoinComponent {
14+
data class NoDataContent(val codeStatus: Int? = null) :
15+
Failure(codeStatus, errorMessageRes = R.string.core_networking_no_data_content)
16+
17+
data class ServerError(val codeStatus: Int? = null) :
18+
Failure(codeStatus, errorMessageRes = R.string.core_networking_no_server_error)
19+
20+
data class GenericError(
21+
val codeStatus: Int? = -12, private val msg: String? = null
22+
) : Failure(
23+
codeStatus
24+
)
25+
26+
data class NetworkError(
27+
val codeStatus: Int? = -13, private val throwable: Throwable
28+
) : Failure(
29+
codeStatus, errorMessageRes = R.string.core_networking_networking_error
30+
)
31+
32+
data class UnknownError(
33+
val codeStatus: Int? = null, private val throwable: Throwable? = Exception()
34+
) : Failure(
35+
codeStatus, throwable?.message
36+
)
37+
38+
data class UnexpectedApiException(
39+
val codeStatus: Int? = -14, private val throwable: Throwable? = Exception()
40+
) : Failure(
41+
codeStatus, throwable?.message
42+
)
43+
44+
data class ClientException(
45+
val codeStatus: Int? = -15, private val throwable: Throwable? = Exception()
46+
) : Failure(
47+
codeStatus, throwable?.message
48+
)
49+
50+
data class UnparsableResponseException(
51+
val codeStatus: Int? = -16, private val throwable: Throwable? = Exception()
52+
) : Failure(
53+
codeStatus, throwable?.message
54+
)
55+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.flow
5+
6+
sealed class NetworkResponse<out T> {
7+
data class Success<out T>(
8+
val value: T
9+
) : NetworkResponse<T>()
10+
11+
data class Error(
12+
val body: Any? = null,
13+
@Transient
14+
val exception: Failure? = null
15+
) : NetworkResponse<Nothing>()
16+
}
17+
18+
fun <T> NetworkResponse<T>.toResult(): Result<T> =
19+
when (this) {
20+
is NetworkResponse.Success -> {
21+
Result.success(this.value)
22+
}
23+
is NetworkResponse.Error -> {
24+
Result.failure(this.exception ?: Failure.GenericError())
25+
}
26+
}
27+
28+
fun <T> NetworkResponse<T>.toFlow(): Flow<T> {
29+
val networkResponse = this
30+
return flow {
31+
when (networkResponse) {
32+
is NetworkResponse.Success -> {
33+
emit(networkResponse.value)
34+
}
35+
is NetworkResponse.Error -> {
36+
throw networkResponse.exception ?: Failure.GenericError()
37+
}
38+
}
39+
}
40+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import kotlinx.coroutines.flow.catch
5+
6+
inline fun <T> Result<T>.onError(action: (exception: Failure) -> Unit): Result<T> {
7+
if(isFailure && exceptionOrNull() is Failure){
8+
val error = exceptionOrNull() as Failure
9+
action(error)
10+
}
11+
return this
12+
}
13+
14+
fun <T> Flow<T>.catchFailure(action: suspend kotlinx.coroutines.flow.FlowCollector<T>.(Failure) -> Unit): Flow<T> =
15+
catch {
16+
if(it is Failure){
17+
action(it)
18+
}else{
19+
action(Failure.GenericError())
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError.coroutines
2+
3+
import com.squareup.moshi.Moshi
4+
import retrofit2.Call
5+
import retrofit2.CallAdapter
6+
import java.lang.reflect.Type
7+
8+
class NetworkResponseAdapter(
9+
private val responseType: Type,
10+
private val moshi: Moshi
11+
): CallAdapter<Type, Any> {
12+
13+
override fun responseType(): Type = responseType
14+
override fun adapt(call: Call<Type>) = NetworkResponseCall(call,moshi)
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError.coroutines
2+
3+
import com.codandotv.streamplayerapp.core_networking.handleError.NetworkResponse
4+
import com.squareup.moshi.Moshi
5+
import retrofit2.Call
6+
import retrofit2.CallAdapter
7+
import retrofit2.Retrofit
8+
import java.lang.reflect.ParameterizedType
9+
import java.lang.reflect.Type
10+
11+
class NetworkResponseAdapterFactory(private val moshi : Moshi): CallAdapter.Factory() {
12+
override fun get(returnType: Type,
13+
annotations: Array<Annotation>,
14+
retrofit: Retrofit
15+
): CallAdapter<*, *>? {
16+
return try {
17+
// suspend functions wrap the response type in `Call`
18+
if (Call::class.java != getRawType(returnType)) {
19+
return null
20+
}
21+
22+
// check first that the return type is `ParameterizedType`
23+
check(returnType is ParameterizedType) {
24+
"return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
25+
}
26+
27+
// get the response type inside the `Call` type
28+
val responseType = getParameterUpperBound(0, returnType)
29+
30+
// if the response type is not ApiResponse then we can't handle this type, so we return null
31+
if (getRawType(responseType) != NetworkResponse::class.java) {
32+
return null
33+
}
34+
35+
// the response type is ApiResponse and should be parameterized
36+
check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }
37+
38+
val successBodyType = getParameterUpperBound(0, responseType)
39+
40+
return NetworkResponseAdapter(successBodyType,moshi)
41+
} catch (ex: ClassCastException) {
42+
null
43+
}
44+
}
45+
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.codandotv.streamplayerapp.core_networking.handleError.coroutines
2+
3+
import com.codandotv.streamplayerapp.core_networking.handleError.Failure
4+
import com.codandotv.streamplayerapp.core_networking.handleError.NetworkResponse
5+
import com.squareup.moshi.Moshi
6+
import okhttp3.Request
7+
import okhttp3.ResponseBody
8+
import okio.Timeout
9+
import retrofit2.Call
10+
import retrofit2.Callback
11+
import retrofit2.HttpException
12+
import retrofit2.Response
13+
import java.io.IOException
14+
15+
class NetworkResponseCall<T>(
16+
proxy: Call<T>,
17+
private val moshi: Moshi
18+
) :
19+
CallDelegate<T, NetworkResponse<T>>(proxy) {
20+
21+
override fun enqueueImpl(callback: Callback<NetworkResponse<T>>) =
22+
proxy.enqueue(object : Callback<T> {
23+
override fun onResponse(call: Call<T>, response: Response<T>) {
24+
val body = response.body()
25+
val code = response.code()
26+
val error = response.errorBody()
27+
if (response.isSuccessful) {
28+
responseSuccessful(body, callback, code)
29+
} else {
30+
responseError(callback,error, code)
31+
}
32+
}
33+
34+
override fun onFailure(call: Call<T>, throwable: Throwable) {
35+
val networkResponse = when (throwable) {
36+
is IOException -> Failure.NetworkError(throwable = throwable)
37+
is HttpException -> convertException(throwable)
38+
else -> Failure.GenericError(msg = throwable.message)
39+
}
40+
callback.onResponse(
41+
this@NetworkResponseCall,
42+
Response.success(NetworkResponse.Error(exception = networkResponse))
43+
)
44+
}
45+
46+
47+
private fun convertException(exception: HttpException): Failure {
48+
return try {
49+
val response = getErrorResponse(exception)
50+
Failure.UnexpectedApiException(throwable = response.exception)
51+
} catch (ex: Failure.ClientException) {
52+
ex
53+
}catch (ex : Failure.UnparsableResponseException){
54+
ex
55+
}
56+
}
57+
58+
private fun getErrorResponse(ex: HttpException): NetworkResponse.Error {
59+
val error = ex.response()?.errorBody()?.string()
60+
if (error?.isEmpty() != false) {
61+
throw Failure.ClientException(throwable = ex)
62+
}
63+
return parseError(error, ex)
64+
}
65+
66+
private fun parseError(error: String, ex: HttpException): NetworkResponse.Error {
67+
try {
68+
return moshi
69+
.adapter(NetworkResponse.Error::class.java)
70+
.fromJson(error)!!
71+
} catch (e: Exception) {
72+
throw Failure.UnparsableResponseException(throwable = ex)
73+
}
74+
}
75+
})
76+
77+
private fun responseSuccessful(
78+
body: T?,
79+
callback: Callback<NetworkResponse<T>>,
80+
code: Int
81+
) {
82+
if (body != null) {
83+
callback.onResponse(
84+
this@NetworkResponseCall,
85+
Response.success(NetworkResponse.Success(body))
86+
)
87+
} else {
88+
callback.onResponse(
89+
this@NetworkResponseCall,
90+
Response.success(
91+
NetworkResponse.Error(
92+
exception = Failure.NoDataContent(
93+
code
94+
)
95+
)
96+
)
97+
)
98+
}
99+
}
100+
101+
private fun responseError(callback: Callback<NetworkResponse<T>>, error: ResponseBody?, code: Int) {
102+
val errorBody = when {
103+
error == null -> null
104+
error.contentLength() == 0L -> null
105+
else -> try {
106+
moshi
107+
.adapter(NetworkResponse.Error::class.java)
108+
.fromJson(error.string())
109+
} catch (ex: Exception) {
110+
null
111+
}
112+
}
113+
if (errorBody != null) {
114+
callback.onResponse(
115+
this@NetworkResponseCall,
116+
Response.success(
117+
NetworkResponse.Error(
118+
body = errorBody,
119+
exception = Failure.ServerError(code)
120+
)
121+
)
122+
)
123+
} else {
124+
callback.onResponse(
125+
this@NetworkResponseCall,
126+
Response.success(
127+
NetworkResponse.Error(
128+
exception = Failure.UnknownError(
129+
code
130+
)
131+
)
132+
)
133+
)
134+
}
135+
}
136+
137+
138+
override fun cloneImpl(): Call<NetworkResponse<T>> =
139+
NetworkResponseCall(proxy.clone(), moshi)
140+
}
141+
142+
abstract class CallDelegate<TIn, TOut>(
143+
protected val proxy: Call<TIn>
144+
) : Call<TOut> {
145+
override fun execute(): Response<TOut> = throw NotImplementedError()
146+
override final fun enqueue(callback: Callback<TOut>) = enqueueImpl(callback)
147+
override final fun clone(): Call<TOut> = cloneImpl()
148+
149+
override fun cancel() = proxy.cancel()
150+
override fun request(): Request = proxy.request()
151+
override fun isExecuted() = proxy.isExecuted
152+
override fun isCanceled() = proxy.isCanceled
153+
154+
abstract fun enqueueImpl(callback: Callback<TOut>)
155+
abstract fun cloneImpl(): Call<TOut>
156+
override fun timeout(): Timeout = proxy.timeout()
157+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<resources>
2+
<string name="core_networking_msg_default_error">Não foi possível concluir. Estamos trabalhando para resolver. Tente novamente em alguns instantes.</string>
3+
<string name="core_networking_no_data_content">Sem conteúdo de dados</string>
4+
<string name="core_networking_no_server_error">Erro no servidor</string>
5+
<string name="core_networking_networking_error">Sem conexão. Verifique o wifi ou dados móveis e tente novamente em alguns instantes.</string>
6+
</resources>

feature-list-streams/build.gradle.kts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
@file:Suppress("UnstableApiUsage")
2-
32
android {
43
namespace = "${Config.packageName}feature_list_streams"
5-
4+
65
buildFeatures {
76
compose = true
87
}
@@ -12,6 +11,7 @@ android {
1211
}
1312

1413
dependencies {
14+
implementation(project(Dependencies.Module.core_networking))
1515
implementation(Dependencies.Koin.koin)
1616
Dependencies.Retrofit.list.forEach { implementation(it) }
1717
Dependencies.Kotlin.list.forEach { implementation(it) }
@@ -22,7 +22,6 @@ android {
2222
Dependencies.Compose.list.forEach { implementation(it) }
2323

2424
Dependencies.UnitTest.list.forEach { testImplementation(it) }
25-
//Dependencies.AndroidTest.list.forEach { androidTestImplementation(it) }
2625

2726
implementation(Dependencies.coil)
2827
}

0 commit comments

Comments
 (0)