Retrofit, Live Data adapter, make response class wrapper generic?

Viewed 2141

I use the LiveData call adapter for retrofit, adapted from the google sample

public interface ApiService {
    @GET("?schema=1.2&form=cjson&byCategories=liveChannels&range=1-500&count=true")
    LiveData<ApiResponse<Page<Media>>> getChannels();
}

I have to use different endpoints so I want to make the ApiResponse generic. ApiResponse would be interface or abstract and have different variants from it for all those endpoints; I dont want to write different LiveData extentions for every class that extends ApiResponse.

I want to do something like:

RetrofitLiveData<R, E extends ApiResponse<R>> extends LiveData<E<R>>

(I know the previous code its not compilable, its just an example for what I want)

and then in retrofit interface I can:

LiveData<Endpoint1ApiResponse<Endpoint1Data>>
LiveData<Endpoint2ApiResponse<Endpoint2Data>>
LiveData<Endpoint3ApiResponse<Endpoint3Data>>


public class LiveDataCallAdapterFactory extends CallAdapter.Factory {

    @Override
    public CallAdapter<?, ?> get(@NonNull Type returnType, @NonNull Annotation[] annotations,
                                 @NonNull Retrofit retrofit) {
        if (getRawType(returnType) != LiveData.class) {
            return null;
        }
        Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
        Class<?> rawObservableType = getRawType(observableType);
        if (rawObservableType != ApiResponse.class) {
            throw new IllegalArgumentException("type must be a resource");
        }
        if (! (observableType instanceof ParameterizedType)) {
            throw new IllegalArgumentException("resource must be parameterized");
        }
        Type bodyType = getParameterUpperBound(0, (ParameterizedType) observableType);
        return new LiveDataCallAdapter<>(bodyType);
    }
}

public class LiveDataCallAdapter<R> implements CallAdapter<R, LiveData<ApiResponse<R>>> {
    private final Type responseType;

    public LiveDataCallAdapter(Type responseType) {
        this.responseType = responseType;
    }

    @Override
    public Type responseType() {
        return responseType;
    }

    @Override
    public LiveData<ApiResponse<R>> adapt(@NonNull Call<R> call) {
        return new RetrofitLiveData<R>(call);
    }
}

public class RetrofitLiveData<R> extends LiveData<ApiResponse<R>> {
    private final Call<R> call;

    public RetrofitLiveData(Call<R> call) {
        this.call = call;
    }

    @Override
    protected void onActive() {
        if (!call.isCanceled() && !call.isExecuted()) {
            call.enqueue(callback);
        }
    }

    private final Callback<R> callback = new Callback<R>() {
        @Override
        public void onResponse(@NonNull Call<R> call, @NonNull Response<R> response) {
            postValue(new ApiResponse<>(response));
        }

        @Override
        public void onFailure(@NonNull Call<R> call, @NonNull Throwable t) {
            postValue(new ApiResponse<>(t));
        }
    };

    public void cancel() {
        if (!call.isCanceled()) {
            call.cancel();
        }
    }
}

public class ApiResponse<T> {
    public final int code;
    @Nullable
    public final T body;
    @Nullable
    public final String errorMessage;

    public ApiResponse(Throwable error) {
        code = 500;
        body = null;
        errorMessage = error.getMessage();
    }

    public ApiResponse(Response<T> response) {
        // .......... not important here
    }

    public boolean isSuccessful() {
        return code >= 200 && code < 300;
    }
}
1 Answers

You can use these classes for reaching your goal.

1- You need a liveDataWrapper for using it in CallAdapterFactory.if you need other types of events you can add it in NetworkData sealed class and use it in your required section

import android.os.Handler
import android.os.Looper
import androidx.lifecycle.MutableLiveData
import retrofit2.HttpException


    class NetworkLiveData<T> : MutableLiveData<NetworkLiveData.NetworkData<T?>>() {

        private lateinit var observer: NetworkData<T?>.() -> Unit

        internal fun onSuccessResponse(body: T?) {
            val result = NetworkData.Success(body)
            postValue(result)
        }

        fun observeForever(f: NetworkData<T?>.() -> Unit) {
            observer = f
            super.observeForever(f)
        }

        internal fun onResponseHttpException(httpException: HttpException) {
            val result = NetworkData.ErrorResponse<T?>(httpException)
            postValue(result)
        }

        internal fun onFailedRequest(t: Throwable) {
            val result = NetworkData.ErrorRequest<T?>(t)
            postValue(result)
        }

        fun onFinish() {
            Handler(Looper.getMainLooper()).post {
                this.removeObserver(observer)
            }
        }

        sealed class NetworkData<T> {
            data class Success<T>(val value: T) : NetworkData<T>()
            data class ErrorResponse<T>(val httpException: HttpException) : NetworkData<T>()
            data class ErrorRequest<T>(val t: Throwable) : NetworkData<T>()

            fun onSuccess(f: (responseBody: T) -> Unit) {
                if (this is Success) {
                    f.invoke(value)
                }
            }

            fun onHttpException(f: (code: Int, message: String) -> Unit) {
                if (this is ErrorResponse) {
                    f.invoke(httpException.code(), httpException.message())
                }
            }

            fun onConnectionFailed(f: (t: Throwable) -> Unit) {
                if (this is ErrorRequest) {
                    f.invoke(t)
                }
            }
        }
    }

2-You should create a CallAdapterFactory for LiveData

import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type


class LiveDataCallAdapterFactory private constructor() : CallAdapter.Factory() {
    companion object {
        operator fun invoke() = LiveDataCallAdapterFactory()
    }

    override fun get(
        returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit
    ): CallAdapter<*, *>? {

        if (getRawType(returnType) != NetworkLiveData::class.java) {
            return null
        }

        if (returnType !is ParameterizedType) {
            throw IllegalStateException(
                "must be parameterized"
            )
        }
        val responseType = getParameterUpperBound(0, returnType)
        return BodyCallAdapter<Any>(responseType)
    }


    private class BodyCallAdapter<T>(
        private val responseType: Type
    ) : CallAdapter<T, NetworkLiveData<T>> {

        override fun responseType() = responseType

        override fun adapt(call: Call<T>): NetworkLiveData<T> {
            val liveData = NetworkLiveData<T>()

            call.enqueue(object : Callback<T> {
                override fun onFailure(call: Call<T>, t: Throwable) {
                    liveData.onFailedRequest(t)
                    liveData.onFinish()
                }

                override fun onResponse(call: Call<T>, response: Response<T>) {
                    if (response.isSuccessful) {
                        liveData.onSuccessResponse(response.body())
                    } else {
                        liveData.onResponseHttpException(HttpException(response))
                    }
                    liveData.onFinish()
                }
            })
            return liveData
        }
    }
}

You can find the complete code with sample in this github repository.

Related