19

So I have a model Model.

public class Model { .... } 

This has two subclasses:

public class SubmodelA extend Model { .... }

and

public class SubmodelB extend Model { .... }

These three are wrapped under Data class.

public class ApiData<T extends Model> {

    public T data;

}

My general response wrapper looks like this:

public class ApiResponse<DATA> {

    DATA data;
}

The "dummy" api operation remains the same:

public interface Endpoints {

    Call<ApiResponse<ApiData>> getData();
}

I have an implementation of retrofit2.Callback to handle the responses:

public class ApiCallbackProxy<T> implements retrofit2.Callback<T> {

    public interface ApiResultListener<RESPONSE_TYPE> {
        void onResult(RESPONSE_TYPE response, ApiError error);
    }

    private ApiResultListener<T> mListener;

    private ApiCallbackProxy(ApiResultListener<T> listener) {
        mListener = listener;
    }

    @Override
    public void onResponse(Call<T> call, Response<T> response) {

    }

    @Override
    public void onFailure(Call<T> call, Throwable t) {

    }

    public static <T> ApiCallbackProxy<T> with(ApiResultListener<T> callback) {
        return new ApiCallbackProxy<>(callback);
    }
}

The ApiClient

public class ApiClient {

    public Endpoints mRetrofit;

    public ApiClient() {
       Retrofit retrofit = new Retrofit.Builder().build();
       mRetrofit = retrofit.create(Endpoints.class);
    }

    public <U extends Model> void getData(ApiResultListener<ApiResponse<ApiData<U>>> callback) {
       //Compiler hits here
       mRetrofit.getData().enqueue(ApiCallbackProxy.with(callback));
    }
}

Compiler hits at ApiCallbackProxy.with(callback) with this error:

enter image description here

So I want depending on where this API call is used in the app to return a different subclass of model or the model itself.

ie.

public static void main (String[] args) {
    ApiClient apiClient = new ApiClient();
    apiClient.getData(listener2);
}


public static final ApiResultListener<ApiResponse<Data<SubmodelA>>> listener = (response, error) -> {};

public static final ApiResultListener<ApiResponse<Data<Model>>> listener2 = (response, error) -> {};

public static final ApiResultListener<ApiResponse<Data<SubmodelB>>> listener3 = (response, error) -> {};
Manos
  • 1,471
  • 1
  • 28
  • 45
  • 1
    What is `ChallengeData`? – Jorn Vernee Jun 01 '17 at 19:37
  • I mean `Payload` :) I changed the actual names to more generic ones. – Manos Jun 01 '17 at 19:41
  • I see, I think you still have a few more errors like that. Would be nice if you could post a [mcve] – Jorn Vernee Jun 01 '17 at 19:43
  • @Manos can you add the declaration code for the field ```mRetrofitApiInterface``` and at least an extract of the enclosing class including its signature (name, type-parameters, super class/interface list...) and key field/methods. Thanks. – Valentin Ruano Jul 13 '17 at 17:06
  • This is just the result of `Retrofit retrofit = new Retrofit.Builder()`, `mRetrofitApiInterface = retrofit.create(Endpoints.class);' – Manos Jul 14 '17 at 12:08
  • @Manos looking at the latest Retrofit 2.x javadocs ```Retrofit.Builder``` does not have a ```create``` method but ```Retrofit``` itself does, so you mean to say that the value in ```mRetrofitApiInterface``` is the result of a ```retrofit.create(Endpoints.class)``` call where ```retrofit``` is typed ```Retrofit``` and not ```Retrofit.Builder```? – Valentin Ruano Jul 14 '17 at 16:05
  • @Manos In any, can you simply state what is the type of the ```mRetrofitApiInterface``` field? The prefix ```m``` is typically use for fields and not temporal/local variables so that is why I'm assuming that it is a field. If such type includes a type-parameter I would like you to also add the signature of the enclosing class. You can edit/improve your question with those details rather than provide them in a comment. – Valentin Ruano Jul 14 '17 at 16:06
  • 5
    It would be a lot easier to answer this if you'd provide a [mcve]. Something we can compile with no other dependencies, and see the error. – Jon Skeet Jul 20 '17 at 12:18
  • Could you add declaration for ApiResponse ? – Oleg Jul 20 '17 at 15:41
  • I proposed one more solution, but I can't but agree with @JonSkeet that the question is done quite poorly, e.g. leaving out class definitions, which can be crucial. I think I found the solution, but I had to use naming like "unknown class" for those. What I don't get is who gave this guy so many upvotes on such a poorly written question. – Vlasec Nov 13 '17 at 10:52
  • You're correct that the question is poorly written. I will try to re-write it. – Manos Nov 13 '17 at 10:53
  • Question is edited. – Manos Nov 13 '17 at 13:02
  • Well, your use of generics is plain wrong, so it makes it a bit hard to tell what exactly needs to be done. The way you're using ApiData in interface `Endpoints`, without giving it any concrete type, is just wrong, and I mentioned it in my answer (I'll update it to include the now mentioned class names). – Vlasec Nov 13 '17 at 14:58

1 Answers1

6

The ApiClient class has a listener that expects ApiData<U> in the response.

The problem is, there is no U. You have an Endpoint, and the endpoint has no generic types, and it returns just ApiData with no concrete type selected for the generic.

This is a case of generics gone wrong. The usual idea would be to make Endpoint generic:

public interface Endpoints<U> {
    Call<ApiResponse<ApiData<U>>> getData();
}

However, what does the Retrofit do? It turns HTTP API into Java interface. And looking at the most simple example at the github repo of Retrofit, it seems pretty clear to me that you are supposed to put interfaces that access some real HTTP endpoint. It is not some abstract GET.

So you'd rather give it a concrete type than make it generic. Do something like:

public interface Endpoints {
    Call<ApiResponse<ApiData<Model>>> getData();
}

I expect Retrofit to deserialize the data in the response to your Model. So, having a concrete class rather than an unset generic type variable is crucial to the successful deserialization. However, you can only use it with listeners being either of the following:

ApiResultListener<ApiResponse<Data<Model>>>
ApiResultListener<ApiResponse<Data<? super Model>>>

Also, in the older part of the question, the ApiResponse<Payload> part where the generic type variable looks like it was Payload class, now that is quite devious :)

Vlasec
  • 5,500
  • 3
  • 27
  • 30
  • Is it wise though to make the Retrofit service interface a generic interface? How am I going to instantiate it ? `mRetrofit = retrofit.create(Endpoints.class);` – Manos Nov 13 '17 at 15:35
  • @Manos I edited the answer after reading the most basic example on Retrofit. What you are doing wrong is, your `Endpoints` is some abstract class (I still find the plural gross, but w/e) that does a `GET /data` on some unspecified HTTP address, with unspecified data type returned. This part is where it goes wrong - you need to give it some concrete type that corresponds to the REST endpoint you call. I called it `MyDummyClass` in the answer. – Vlasec Nov 14 '17 at 10:06
  • For the purposes of the question, retrofit doesn't affect anything. I just happened to experience the error while working with it. That why I didn't provide a `@GET` or `@POST` annotation because it's simply irrelevant. Also, introducing a new class seems redundant to me, since the hypothetical HTTP Request returns an `ApiData`. I can't really understand why something like `Call>> getData();` or `Call>> getData();` doesn't work. Obviously, I have no strong consolidation of how generics work. An explanation would be nice. – Manos Nov 14 '17 at 10:19
  • Um ok. Looks like some of the irrelevant parts might be causing the trouble. I'm pretty sure I would find out what's the problem if I saw the actual code, this is just layers of abstraction resulting in us wasting our time on your bounty. – Vlasec Nov 14 '17 at 10:21
  • All the code I use is in the `EDITED` part of the answer. Time is money friend, so please don't waste yours on my bounty! – Manos Nov 14 '17 at 10:24
  • OK, one more edit, now this might work, but seriously, it feels like peeling an onion. Layer after layer, and almost makes you wanna cry, damn. – Vlasec Nov 14 '17 at 10:27
  • I tried both of these as an input to the `ApiClient#getData` method. Nor of these works. By the way why `ApiResponse` and not `ApiResponse`, since the generic of Data can be either a `Model`, a `SubmodelA` or a `SubmodelB` ? I tried so many combinations that i start to believe that this is not doable. Nor it's useful anymore but it triggered my curiosity and I thought that it's a good chance for me to understand generics better. – Manos Nov 14 '17 at 10:54
  • Not sure what exactly the "doesn't work" part means. I guess the compiler error is gone, but something else is wrong. The ` super Model>` should work because a more generic listener that accepts even `Object` should be able to listen to just anything, the ` extends Model>` won't work, because listener of `SubModelA` shouldn't listen to `SubModelB` as it would be confused by the input and cast a `ClassCastException` at runtime, if you made it compile somehow. – Vlasec Nov 14 '17 at 11:03
  • One more thing: Inheritance in DTOs will cause you more harm than good. I don't like this `SubModelA` and `SubModelB` idea. More trouble than it's worth. So, the HTTP endpoint returns different model in different calls to the same resource? Can you somehow merge it into one unsightly model? – Vlasec Nov 14 '17 at 11:05
  • It can be merged, yes. As I already said, this use case is no longer used. The reason that I started the bounty was so I can understand WHY this doesn't work and also to get a better idea of generics. – Manos Nov 14 '17 at 14:10