Unable to make Retrofit2 interface generic for all Models

Viewed 3124

I'm using Retrofit2 in my app and it works really fine, but i am trying to use a single interface for all kind of GET method requests by passing a dynamic model. When i try it using following code, it gives error at runtime

public interface LoadDataServiceTest<T> {
        @GET
        Call<T> getModel(@Url String url, @QueryMap Map<String, String> options);
    }

Model:

public class ModelTest<T> {
    @SerializedName("status")
    private String status;
    @SerializedName("message")
    private String message;
    @SerializedName("users")
    private T data;

    public String getStatus() {
        return status;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }
}

But when i create service as following, then i get error. How can i resolve it and what is the best approach to achieve this goal.

LoadDataServiceTest<ModelTest<JsonArray>> service = retrofit.create((Class<LoadDataServiceTest<ModelTest<JsonArray>>>) (Class<?>) APIs.LoadDataServiceTest.class);
        Map<String, String> parameters = new HashMap<>();
        parameters.put("user_id",userId);
        Call<ModelTest<JsonArray>> call = service.getModel(APIs.GET_USERS, parameters);

Error:

FATAL EXCEPTION: main Process: com.iu.colgatepalmolive, PID: 16808 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.iu.colgatepalmolive/com.iu.hfl_ccp.LoginActivity}: java.lang.IllegalArgumentException: Method return type must not include a type variable or wildcard: retrofit2.Call for method LoadDataServiceTest.getModel at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2666) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2727) at android.app.ActivityThread.-wrap12(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1478) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6121) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779) Caused by: java.lang.IllegalArgumentException: Method return type must not include a type variable or wildcard: retrofit2.Call for method LoadDataServiceTest.getModel at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:720) at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:711) at retrofit2.ServiceMethod$Builder.createCallAdapter(ServiceMethod.java:224) at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:160) at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:166) at retrofit2.Retrofit$1.invoke(Retrofit.java:145) at java.lang.reflect.Proxy.invoke(Proxy.java:813) at $Proxy5.getModel(Unknown Source)

1 Answers

I use following approach. At first I have implemented custom Call

public class ProxyConvertCall<Tin,Tout> implements Call<Tout> {
    Converter<Tin,Tout> converter;
    Call<Tin> innerCall;

    public ProxyConvertCall(Call<Tin> jcall, Converter<Tin,Tout> converter){
        this.innerCall = jcall;
        this.converter = converter;
        }

    @Override
    public Response<Tout> execute() throws IOException {
        Response<Tin> response = innerCall.execute();
        if (response.isSuccessful()){
            return Response.success(converter.Convert(response.body()),response.raw());
        }
        else return Response.error(response.code(), response.errorBody());
    }

    @Override
    public void enqueue(final Callback<Tout> callback) {
        final Call<Tout> self = this;
        this.innerCall.enqueue(new Callback<Tin>() {  
            @Override
            public void onResponse(Call<Tin> call, Response<Tin> response) {
                if (response.isSuccessful()){
                    callback.onResponse(self, Response.success(converter.Convert(response.body()), response.raw()));
                }
                else callback.onResponse(self, Response.error(response.code(), response.errorBody()));
            }
            @Override
            public void onFailure(Call<Tin> call, Throwable t) {
                callback.onFailure(self,t);
            }
        });

    }

    @Override
    public boolean isExecuted() {
        return innerCall.isExecuted();
    }

    @Override
    public void cancel() {
        innerCall.cancel();

    }

    @Override
    public boolean isCanceled() {
        return innerCall.isCanceled();
    }

    @Override
    public Call<Tout> clone() {
        return new ProxyConvertCall<>(innerCall,converter);
    }

    @Override
    public Request request() {
        return innerCall.request();
    }
}

It adapts Call<Tin> to Call<Tou> using a converter.

@FunctionalInterface 
public interface Converter<Tin, Tout> {
    public Tout Convert(Tin in);
}

For your service you must create service interface, that return JsonObject for single object and JsonArray for arrays

  public interface LoadDataServiceTest {
     @GET
     public Call<JsonObject> getModelTest(@Url String url, @QueryMap Map<String, String> options);

     @GET
     public Call<JsonObject> getModel(@Url String url, @QueryMap Map<String, String> options);

     @GET
     public Call<JsonArray> getModels(@Url String url, @QueryMap Map<String, String> options);


}

Then wrap it with generic class, with converters from JsonElement to any Type <T> and ModelTest<T>:

   public class LoadDataServiceTestGeneric<T> {
     Converter<JsonObject,ModelTest<T>> fromJsonObjectToModelTest;
     Converter<JsonObject,T> fromJsonObject;
     Converter<JsonArray,List<T>> fromJsonArray;
     LoadDataServiceTest service;

    public LoadDataServiceTestGeneric(Class<T> classOfT, LoadDataServiceTest service){  
        this.service = service;
        Gson gson  = new GsonBuilder().create();
        GenericListType<T> genericListTypeOfT = new GenericListType<T>(classOfT);
        fromJsonObject = (t)->gson.fromJson(t,classOfT);
        fromJsonArray =(t)->gson.fromJson(t,genericListTypeOfT);
    }
    public Call<ModelTest<T>> getModelTest(String url, Map<String, String> options){ 
        return new ProxyConvertCall<>(service.getModelTest(url, options), fromJsonObjectToModelTest);
    }

    public Call<T> getModel(String url, Map<String, String> options){ 
        return new ProxyConvertCall<>(service.getModel(url, options), fromJsonObject);
    }

    Call<List<T>> getModels(String url, Map<String, String> options){ 
        return new ProxyConvertCall<>(service.getModels(url, options), fromJsonArray);
    }

}

GenericListType is ParameterizedType. It is used for passing type parameter to gson for List<T>

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;

public class GenericListType<T> implements ParameterizedType {

    private Type wrapped;

    public GenericListType(Type wrapped) {
        this.wrapped = wrapped;
    }

    public Type[] getActualTypeArguments() {
        return new Type[] {wrapped};
    }

    public Type getRawType() {
        return  List.class;
    }

    public Type getOwnerType() {
        return null;
    }

}

ModelTestType is ParameterizedType. It is used for passing type parameter to gson for ModelTestType<T>

public class ModelTestType  implements ParameterizedType {

        private Type wrapped;

        public ModelTestType(Type wrapped) {
            this.wrapped = wrapped;
        }

        public Type[] getActualTypeArguments() {
            return new Type[] {wrapped};
        }

        public Type getRawType() {
            return  ModelTest.class;
        }

        public Type getOwnerType() {
            return null;
        }

    }

Then you can instantiate LoadDataServiceTestGeneric with type you want.

Related