https://kotlinlang.org logo
Title
l

Lukasz Kalnik

07/06/2022, 4:03 PM
I have already one
CallAdapter
for Retrofit which converts the responses to
Either<Failure, Success>
type (from the Arrow.kt library). Now I would like to attach something in the call chain, if the call result is an
IOException
then I want to notify the UI that possibly the network connection is gone. Is there some mechanism to do it? Currently I tried to attach another
CallAdapter
just for this purpose (which basically just should pass the Call unchanged, but as a side-effect emit an event to the UI in the
onFailure()
callback of the
Call<T>
). However it doesn't seem to work when put as the first
CallAdapter
in chain. There is an error that Moshi converter tries to already parse the call after the first adapter (not waiting for the second one, which outputs the
Either<F, S>
type). It complains that it cannot parse JSON to a
Call<T>
object.
z

Zac Sweers

07/06/2022, 4:18 PM
Bit of a plug but you could look at how we implement something similar in GitHub.com/slackhq/eithernet
So the conversion to a sealed error hierarchy is solved for me.
What I want to do though, is also to emit a side effect when
IOException
is received.
And I don't want to do it in the existing
CallAdapter
(because I get it from the library and I don't want to fork it). I want to add another adapter just for this case (or something like this). I guess your library cannot really support me in emitting this side effect, right?
My question is "How do I create a
CallAdapter
which just passes the request completely unchanged and only emits a side effect under some condition?"
I.e. so that the UI is notified from a central point (and not specifically at the API call site) that an
IOException
has occurred. Currently we have in every Presenter/ViewModel the check for this:
apiService.getSomeData().fold(
    ifLeft = { if(it is IOError) showConnectionLost() },
    ifRight = { /* success case */ }
)
(left/right are error/success cases in Arrow Either type). And it's very repetitive to check every time if the error is
IOError
.
z

Zac Sweers

07/06/2022, 7:36 PM
model your failure type as another sealed type and make network errors as one of its subtypes?
l

Lukasz Kalnik

07/06/2022, 7:37 PM
Yes, I already have a sealed failure type hierarchy.
l

Lukasz Kalnik

07/06/2022, 7:38 PM
My problem is having to call the
onConnectionLost()
from every Api consumer. I want to have a central place where it is being intercepted for every Api call.
z

Zac Sweers

07/06/2022, 7:39 PM
you want retrofit to show UI for that or…?
l

Lukasz Kalnik

07/06/2022, 7:40 PM
Yes, I want Retrofit to emit an event (e.g. via
Channel
) as a side effect of receiving an IOException
The example you linked is already in the Adapter I'm using.
It works.
z

Zac Sweers

07/06/2022, 7:41 PM
create another call adapter that delegates to it and watches for that sealed subtype? ¯\_(ツ)_/¯
l

Lukasz Kalnik

07/06/2022, 7:41 PM
But I don't want to fork the library Adapter (by adding a side effect). I want to add another one
Yes, I tried creating another adapter but I cannot get it to work 😞
Probably I'm doing something wrong because Moshi tries to parse the result of the second adapter...
I will link the code I'm trying to write here tomorrow (it's already night here in Germany).
Basically I want this other adapter to just pass the call through unchanged, only emitting the side effect when the result is of specific type. But it returns a
Call<T>
and then Moshi tries to parse it.
j

jw

07/07/2022, 12:30 AM
Your body type is wrong then
You're returning the full type instead of unwrapping the body type
Are you returning delegate.responseType() from your response type function?
l

Lukasz Kalnik

07/07/2022, 7:43 AM
Oh yes, I return the plain
returnType
. I should unwrap it. Thanks for the hint!
I unwrapped the body type by means of
getParameterUpperBound(0, returnType)
. I still get the error for not having a converter, although now for the unwrapped type:
java.lang.IllegalArgumentException: Unable to create converter for arrow.core.Either<? extends arrow.retrofit.adapter.either.networkhandling.CallError, de.myapp.gatewayapi.data.remote.air.model.ControlDeviceList>
I suspect the problem is that I now have registered two call adapter factories:
Retrofit.Builder()
                .baseUrl(baseUrl)
                .client(client)
                .addCallAdapterFactory(IOExceptionCallAdapterFactory())
                .addCallAdapterFactory(EitherCallAdapterFactory())
                .addConverterFactory(MoshiConverterFactory.create(moshiForKotlin))
                .build()
                .create(AirConnectorApi::class.java)
The first one,
IOExceptionCallAdapterFactory
, creates the pass-through adapter, which should not modify the call in any way but only look if there was an
IOException
and emit a side effect for the UI. The second one,
EitherCallAdapterFactory
, creates the actual adapter which type-safely wraps success results in
Either.Right
and failures or exceptions in
Either.Left
. That means that I want to skip the converter created by
MoshiConverterFactory
after the first adapter and only call it after both of the adapters have run. Only then can the response body actually be deserialized. Is it possible to configure Retrofit to only call the converter after all adapters have run?
j

jw

07/07/2022, 11:46 AM
Seems like your adapter order is backwards
l

Lukasz Kalnik

07/07/2022, 11:46 AM
Is the last one run first?
Yes, when I reverse the adapters the app doesn't crash.
But the
println
which I put into `onResponse()`/`onFailure()` inside of
IOExceptionCall.enqueque()
(
IOExceptionCall
is returned by the pass-through adapter) doesn't register in logs... Looks like the second adapter is not being called at all.