I have a mobile application that makes HTTP requests and waits for responses standardized by JSON attributes. I need to throw an Exception globally when a certain attribute is false.
Example:
HTTP RESPONSE 200 OK
{
"data": [],
"message": "An error was happened!",
"success": false
}
In the example above, the HTTP response returned status code 200, but the 'success' attribute is signaling that an error has occurred.
I need to make Retrofit understand that this attribute threw an exception from attr 'message'.
Googling I managed to find some examples where I was successful, using the Custom Call Adapter for this purpose:
class RxErrorHandlingCallAdapterFactory: CallAdapter.Factory() {
companion object {
fun create() : CallAdapter.Factory = RxErrorHandlingCallAdapterFactory()
}
private val _original by lazy {
RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io())
}
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
val wrapped = _original.get(returnType, annotations, retrofit) as CallAdapter<out Any, *>
return RxCallAdapterWrapper(wrapped)
}
private class RxCallAdapterWrapper<R>(
val _wrappedCallAdapter: CallAdapter<R, *>
) : CallAdapter<R, Observable<R>> {
override fun responseType(): Type = _wrappedCallAdapter.responseType()
#SuppressLint("CheckResult")
override fun adapt(call: Call<R>): Observable<R> {
val adapted = (_wrappedCallAdapter.adapt(call) as Observable<R>)
if ((responseType() == DefaultResponseModel::class.java
|| (responseType() as Class<R>).superclass == DefaultResponseModel::class.java)
) {
(adapted as Observable<DefaultResponseModel>).blockingFirst()?.let {
if (it.success == false) {
throw Throwable(it.message)
}
}
}
return adapted
}
}
}
The problem in fact is that with each HTTP request my application literally crashes for a few long seconds, causing undesirable black screens.
Analyzing the code I managed to find the cause of the slowness, but I don't know how to solve the problem.
(adapted as Observable<DefaultResponseModel>).blockingFirst()
The code snippet above gets the Model of the 'DefaultResponseModel' class from the Stream of the Observable. But for some reason this causes an exaggerated slowdown in the App with each request.
I would be grateful if someone could help me with this endeavor :D
--
Libraries:
Reotrofit 2.6
Kodein 6.4.1
Rxjava2 2.1.1
Related
My final goal here is to have the following retrofit interface declared:
interface SomeService {
#GET("/path")
fun getSomethingFlow() : Flow<DataState<SomeApiDataClass>>
}
data class DataState<T>(val data : T, state: State)
// the real data class is a big bigger, but that's the idea
// state is an enum
The intention is that my CallAdapter will handle the Flow and the DataState and let Moshi handle the T.
I've been following mostly from here https://github.com/MohammadSianaki/Retrofit2-Flow-Call-Adapter/tree/master/FlowAdapter/src/main/java/me/sianaki/flowretrofitadapter
So I created a CallAdapter<T, Flow<DataState<T>>> with override fun adapt(call: Call<T>): Flow<DataState<T>> that does all the processing I want it to do.
I've created the CallAdapter.Factory() and all the API data classes have Moshi annotation #JsonClass(generateAdapter = true).
here is the CallAdapter.Factory:
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Flow::class.java) {
return null
}
if (returnType !is ParameterizedType) {
return null
}
val flowType = getParameterUpperBound(0, returnType)
val rawFlowType = getRawType(flowType)
if (rawFlowType != DataState::class.java) {
return null
}
return MyCallAdapter(flowType)
}
and relevant CallAdapter code:
override fun adapt(call: Call<T>): Flow<DataState<T>> {
Log.d("TEST", "adapt $call")
// do all my logic here.
This method is never called!!!
return dataState
}
override fun responseType(): Type {
Log.d("TEST", "Retrofit is asking us the type")
return returnType
}
At the end I got my Retrofit building like:
val api = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(MyAdapterFactory())
.client(client)
.baseUrl("https://.... ")
.build()
.create(SomeService::class.java)
When I try to call the API I get the following crash:
java.lang.IllegalArgumentException: Unable to create converter for com.package.DataState<com.package.SomeServiceDataClass>
for method SomeService.getSomethingFlow
at retrofit2.Utils.methodError(Utils.java:54)
at retrofit2.HttpServiceMethod.createResponseConverter(HttpServiceMethod.java:126)
at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:85)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:39)
.....
Caused by: java.lang.IllegalArgumentException: Cannot serialize Kotlin type com.package.DataState Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapterFactory from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.
at com.squareup.moshi.ClassJsonAdapter$1.create(ClassJsonAdapter.java:97)
at com.squareup.moshi.Moshi.adapter(Moshi.java:145)
at com.squareup.moshi.Moshi.adapter(Moshi.java:105)
It seems that even thou I gave to Retrofit a CallAdapter from Call<T> to Flow<DataState<T>>, it still trying to use data converters (moshi) for parsing the DataState.
On debugging I found that the Factory passes all the checks and returns a new MyCallAdapter and that Retrofit calls my MyCallAdapter.responseType(). So it's mostly there.
The fun adapt(call: Call<T>): Flow<DataState<T>> is never called!
So the question is:
How do I tell retrofit to only use Moshi for T and leave the DataState to me?
After crying a bit and re-considering some life choices, I found the solution.
It is a good ol' case of "should have better read the docs".
Check it out!!
Returns the value type that this adapter uses when converting the HTTP
response body to a Java object. For example, the response type for
Call is Repo. This type is used to prepare the call passed to
#adapt. Note: This is typically not the same type as the returnType provided to this call adapter's factory.
So after updating the Factory this following snippet, it all works.
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
val typeOfFlow = getInnerType(returnType, Flow::class.java) ?: return null
val typeOfDataState = getInnerType(typeOfFlow, DataState::class.java)?: return null
return MyAdapter(typeOfDataState)
}
private fun getInnerType(type: Type, klazz: Class<*>): Type? {
if (type !is ParameterizedType) {
return null
}
if (getRawType(type) != klazz) {
return null
}
return getParameterUpperBound(0, type)
}
I'm trying to mock response to my query but I can't because the builder needed and doesn't know how to pass operations?
Basicly I have network class like this :
class NetworkService #Inject constructor(
private val apolloClient: ApolloClient
) {
suspend fun <D : Operation.Data, T, V : Operation.Variables> suspendedQuery(
query: Query<D, T, V>,
cachePolicy: HttpCachePolicy.Policy = HttpCachePolicy.NETWORK_FIRST
): Resource<Response<T>> {
val response = try {
apolloClient.query(query)
.toBuilder().httpCachePolicy(cachePolicy)
.build()
.await()
} catch (e: ApolloException) {
return Resource.error(e.localizedMessage)
}
return if (response.hasErrors()) {
Resource.error(response.errors.toString())
} else {
Resource.success(response)
}
}
}
and I want to mock the response returned from this function like this
I successed when I'm returning an error
val expectedResponse = Resource.error<Response<MyQuery.Data>>("ERROR")
But I have problem to mocking the resposne here:
val expectedResponse = Resource.success<Response<MyQuery.Data>>(Response("Response.builder(Operation<>)"))
I want to know How to build the inside the quote "Response.builder(Operation<>)"?
It would appear that this is a long running problem according to the apollo-android issues list. I can see that you've asked a similar question there also.
I found this issue which looks to be resolving this problem in the not too distant future. It was supposed to be this month but now looks like next.
So think this means that you're only solution right now is to use mock web server, and have it return a sample response to the client. This is how we have implemented our tests currently.
I've spent hours trying to figure this thing out, and I still can figure it out.
I'm trying to retrieve data from a website using JSON.
If the website is live and everything, it works, but if the website returns something else than the data, like a 403 error, or any other error, then it crashes. I tried to debug it, but I still don't understand what is going on here.
Here is my code:
I have a NetworkModule with an interceptor that is supposed to check is the response is valid or not, and from what I can tell it works, because my variable isDataRetrievable is false (the value by default):
val networkModule = module {
single {
val customGson =
GsonBuilder().registerTypeAdapter(Lesson::class.java, LessonDeserializer())
.create()
Retrofit.Builder()
.client(get())
.addConverterFactory(
GsonConverterFactory.create(customGson)
)
.baseUrl(BuildConfig.URL)
.build()
}
factory {
OkHttpClient.Builder()
.addInterceptor(Interceptor { chain ->
chain.withConnectTimeout(1,TimeUnit.SECONDS)
val request: Request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful){
networkStatus.isDataRetrievable = true
}
response
}).build()
}
factory {
get<Retrofit>().create(LessonApi::class.java)
}
}
Next, I have my API to get the data:
interface LessonApi {
#GET("/JSON/json_get_data.php")
suspend fun getLessons(): Call<Lesson>
}
Then, for some reason, I have a repository (I'm not the only one working on this code, I didn't do this part):
class LessonRepository(private val service: LessonApi) {
suspend fun getLessons() = service.getLessons()
}
Then, I have my splash screen view model, that is supposed to retrieve the data if possible:
if (networkStatus.isNetworkConnected && networkStatus.isWebsiteReachable) {
var tmp = repository.getLessons()
tmp.enqueue(object : Callback<Lesson> {
override fun onFailure(call: Call<Lesson>, t: Throwable) {
Log.d("DataFailure",t.message.toString())
nextScreenLiveData.postValue(false)
}
override fun onResponse(call: Call<Lesson>, response: Response<Lesson>) {
Log.d("DataFailure","Test")
}
})
}else{
nextScreenLiveData.postValue(false)
}
The problem is that when the program get to the line repository.getLessons(), it crashes with the error:
retrofit2.HttpException: HTTP 403
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:49)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:129)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
So onFailure or onResponse are never called. I tried to run the debugger, to step in, but I cannot figure it out when it fails.
I thought it was because it was trying to deserialize invalid data, but I put breakpoints everywhere in my deserializer, and it never hits the breakpoints.
I'm not a professional android developer, but I'm very confused here.
What I'd like to do is that if the request is unsuccessful, just discard the response (do not deserialize it), and display a message or exit.
Please help, it's so frustrating. I'm not sure how to intercept errors or what to do if Interceptors get an unsuccessful request (for now I just set a variable but it's unused).
Thanks.
Edit: What I'm trying to do, is to retrieve data from a webserver. If it cannot (for any reason), I don't want the gson to parse data (because it will probably be garbage and will not correspond to my deserializer). However, I feel like this okhttp / retrofit is a pipeline, where okhttp get the response from the webserver and pass it to a gson converter. What I want to do is intercept this response, and if it's not successful, to NOT pass it to gson, set a variable, so that the rest of my application knows what to do. But the thing is, for now, it just crash even before it gets to the callback in enqueue. The interceptor works just fine, except I'd like him to drop the response if it's not successful. Is it possible?
I tried something like that, and it worked to handle bad codes (>400), but I also wanted to handle malformed JSON data, so I added the onResponse and onFailure callbacks, but it never worked, because when I receive a malformed JSON, it also trigger an exception, and then go in the catch before it can go on the 'enqueue', so I'm not sure what this is used for.
try {
val lessons = repository.getLessons().enqueue(object : Callback<List<Lesson>> {
override fun onResponse(call: Call<List<Lesson>>, response: Response<List<Lesson>>) {
networkStatus.isDataRetrievable = response.isSuccessful
Log.d("Retrofit", "Successful response")
nextScreenLiveData.postValue(response.isSuccessful)
}
override fun onFailure(call: Call<List<Lesson>>, t: Throwable) {
Log.d("Retrofit", "Failure response")
nextScreenLiveData.postValue(false)
}
})
nextScreenLiveData.postValue(true)
} catch (e: Exception) {
nextScreenLiveData.postValue(false)
}
Anyway, just this code works for everything in the end:
try {
val lessons = repository.getLessons().filter {
it.lesson.contains("video")
}.filter {
DataUtils.isANumber(it.id)
}
lessonDao.insertLessons(lessons)
networkStatus.isDataRetrievable = true
} catch (e: Exception) {
networkStatus.isDataRetrievable = false
}
But in my API, I don't return callbacks, I directly return the objects, as such:
#GET("/JSON/json_get_dat.php")
suspend fun getLessons(): List<Lesson>
I don't know if this is the right way to do it, but it works. I hope this might help others.
i got solution of this question: Wildcards generic in Kotlin for parameter, but now i have other question which still related to kotlin generic
I have an abstract class using for listen api callback like below. ApiRs is a parent object that every API response object inherit from it
abstract class ApiCallback<in T : ApiRs> {
open fun onSucceed(apiRsModel: T) {}
open fun onFailed(code: Int,
message: String) {
}
}
this time i write a function to handle api succeed with Retrofit2, than check something and callback to UI, here is my function:
fun <T : ApiRs> callbackWithSucceed(apiCallback: ApiCallback<T>?,
context: Context,
response: Response<out ApiRs>?) {
// unexpected error
if (response == null) {
encounterUnexpectedError(apiCallback, context, null)
return
}
// check http
val httpSucceed = response.code() == CODE_HTTP_SUCCEED
&& response.isSuccessful
&& response.body() != null
if (!httpSucceed) {
// HTTP response with error
callbackWithFailed(
apiCallback,
response.code(),
response.message())
return
}
apiCallback?.onSucceed(response.body()!!)
}
}
response is Retrofit2 class, which contains my API response model (body) and every response model are inherit ApiRs, my goal is pass the model to the abstract class using this way apiCallback?.onSucceed(response.body()!!) but it will show an error
Type mismatch, required T? but Found ApiRs?
Sorry about i have bad concept of generic. I think function open fun onSucceed(apiRsModel: T) {} T should inherit ApiRs because i defined in class, so i cannot understand why error message shown?
Your response: Response<out ApiRs>? has to be response: Response<out T>?, then the error should be gone.
The type of T which will be passed to the method onSuccess must match the generic type of the apiCallback parameter, which isn't <ApiRs> but <T>.
This scene takes place in an Android app using Retrofit2 and Moshi for JSON deserialization.
In a case where you don't have control over the server's implementation, and this said server have an inconsistent behavior in how it answers requests (also know as "a bad case"):
Is there a way to handle com.squareup.moshi.JsonDataException without crashing?
For example you expected a JSONArray, and here comes a JSONObject. Crash. Is there another way to handle this than having the app crashing?
Also in the case the server's implementation is updated, wouldn't it be better to display an error message to the user, instead of having it to crash / be totally out of service, even for one wrong request?
Make the call with Retrofit and use try and catch to handle exceptions, something similar to:
class NetworkCardDataSource(
private val networkApi: NetworkCardAPI,
private val mapper: CardResponseMapper,
private val networkExceptionMapper: RetrofitExceptionMapper,
private val parserExceptionMapper: MoshiExceptionMapper
) : RemoteCardDataSource {
override suspend fun getCard(id: String): Outcome<Card, Throwable> = withContext(Dispatchers.IO) {
val response: Response<CardResponseJson>
return#withContext try {
response = networkApi.getCard(id)
handleResponse(
response,
data = response.body(),
transform = { mapper.mapFromRemote(it.card) }
)
} catch (e: JsonDataException) {
// Moshi parsing error
Outcome.Failure(parserExceptionMapper.getException(e))
} catch (e: Exception) {
// Retrofit error
Outcome.Failure(networkExceptionMapper.getException(e))
}
}
private fun <Json, D, L> handleResponse(response: Response<Json>, data: D?, transform: (D) -> L): Outcome<L, Throwable> {
return if (response.isSuccessful) {
data?.let {
Outcome.Success(transform(it))
} ?: Outcome.Failure(RuntimeException("JSON cannot be deserialized"))
} else {
Outcome.Failure(
HTTPException(
response.message(),
Exception(response.raw().message),
response.code(),
response.body().toString()
)
)
}
}
}
where:
networkApi is your Retrofit object,
mapper is a class for mapping the received object to another one used in your app (if needed),
networkExceptionMapper and parserExceptionMapper map Retrofit and Moshi exceptions, respectively, to your own exceptions so that Retrofit and Moshi exceptions do not spread all over your app (if needed),
Outcome is just a iOS Result enum copy to return either a Success or a Failure result but not both,
HTTPException is a custom Runtime exception to return unsuccessful request.
This a snippet from a clean architecture example project.