AntCopp
AntCopp

Reputation: 163

Convert retrofit callback value to return enveloped object

My question is about the possibility to use RxJava for Android to manipulate data from a Retrofit call.

I've just started to use these libraries, so if my questions are trivial, please be patient.

This is my scenario.

I have a json returned from the server that looks like this

  { <--- ObjectResponse
     higher_level: {
        data: [
            {
               ...
               some fields,
               some inner object: {
               ....
                },
               other fields
            }, <----- obj 1

            ....,

            {
               ....
            }<---- obj n

         ]<-- array of SingleObjects

      }

  } <--- whole ObjectResponse

I've already have retrofit get this response and parsed in a ObjectResponse. Parsing this object, I can obtain a List that I can pass as usual to my RecyclerView Adapter.

So retrofit returned the ObjectResponse which is the model for the entire server answer, and in the retrofit callback I manipulate ObjectResponse to extract my List to be then passed to my adapter.

Right now, I have something like this

Call<ObjectResponse> call = apiInterface.getMyWholeObject();

call.enqueue(new Callback<ObjectResponse>() {
            @Override
            public void onResponse(Call<ObjectResponse> call, Response<ObjectResponse> response) {

                //various manipulation based on response.body() that in the ends 
                // lead to a List<SingleObject>

               mView.sendToAdapter(listSingleObject)
              }

            @Override
            public void onFailure(Call<ObjectResponse> call, 
                  Throwable t) {
                t.printStackTrace();
            }
         });

My question is:

Is there a way to obtain from retrofit an Observable that can ultimate lead me to emit the list of SingleObject (and manipulate it) without have to manipulate ObjectResponse as I would do in the retrofit callback? Or should I have to stick with the retrofit callback and only after obatin List I can manipulate with RxJava just before feed this list to my Adapter?

I'd like to obtain something like this

        apiInterface
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Observer<List<SingleObject>>() {
                @Override
                public void onCompleted() {

                }

                @Override
                public void onError(Throwable e) {

                }

                @Override
                public void onNext(List<Post> posts) {

                  mView.sendToAdapter(listSingleObject)
                }
            });

Upvotes: 1

Views: 4514

Answers (3)

azizbekian
azizbekian

Reputation: 62209

Retrofit converter factories solve that issue very nicely.

Jake Wharton talks about "envelope" objects in his "Making Retrofit Work For You" talk and points out how that can be solved.

Having defined an envelope class - a class with some extra fields that you do not care about:

public class Envelope<T> {
    Meta meta;
    List<Notification> notifications;
    T response;
}

In this POJO fields meta and List<Notification> are being returned from the backend, but in the context of android app they are not interesting to us. Assume, that the real value that you need from the response is field named response, which might be any object (because it's generic).

Particularly in your example the POJO structure would be like this:

public class OriginalResponse {
    HigherLevel higher_level;
}

public class HigherLevel {
    Data data;
}

public class Data {
    List<ActualData> list;
}

You have to implement your custom Converter.Factory:

public class EnvelopingConverter extends Converter.Factory {
    @Nullable
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        Type envelopedType = TypeToken.getParameterized(Envelope.class, type).getType();

        Converter<ResponseBody, Envelope<?>> delegate = retrofit.nextResponseBodyConverter(this, envelopedType, annotations);
        return body -> {
            Envelope<?> envelope = delegate.convert(body);
            // Here return the object that you actually are interested
            // in your example it would be:
            // originalResponse = delegate.convert(body);
            // return originalResponse.higher_level.data.list;
            return envelope.response;
        };
    }
}

Add this converter to your retrofit builder:


    new Retrofit.Builder()
            ...
            .addConverterFactory(new EnvelopingConverter())
            ...

Then in your retrofit api interface instead of returning Single<OriginalResponse> return Single<List<ActualData>> directly:

interface Service {
    @GET(...)
    Single<List<ActualData>> getFoo();
}

Typical implementation in Kotlin:

class EnvelopeConverterFactory : Converter.Factory() {
  override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
    val envelopedType: Type = TypeToken.getParameterized(ParentObject::class.java, type).type

    val delegate: Converter<ResponseBody, ParentObject> =
      retrofit.nextResponseBodyConverter(this, envelopedType, annotations)

    return Converter<ResponseBody, ChildObject> { body -> delegate.convert(body)?.childObject }
  }
}

Upvotes: 6

AntCopp
AntCopp

Reputation: 163

After some days, I can post my own solution.

It is inspired by the idea suggested by azizbekian.

The center idea is on the Envelope class, which I've express using retrofit annotation to be sure it would adapt to different JSON response from server, parsing the

higher_level: {
        data: [
                mid_level: {  .. },
                ...
              ]
}

structure that I've already explained in my original post

public class WrapperResponse<T> {
    @SerializedName(value="higher_level", alternate={"mid_level", "other"})
    @Expose
    DataResponse<T> data;

    public DataResponse<T> getData() {
        return data;
    }

    public void setData(DataResponse<T> data) {
        this.data = data;
    }
}

The focus here is in the parameters of SerializedName, where I specify all the possible JSON objects name that appear in my server response.

Then I have

public class UnwrapConverterFactory extends Converter.Factory {

    private GsonConverterFactory factory;

    public UnwrapConverterFactory(GsonConverterFactory factory) {
        this.factory = factory;
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(final Type type,
                                                            Annotation[] annotations, Retrofit retrofit) {
        Type wrappedType = new ParameterizedType() {
            @Override
            public Type[] getActualTypeArguments() {
                return new Type[] {type};
            }

            @Override
            public Type getOwnerType() {
                return null;
            }

            @Override
            public Type getRawType() {
                return WrapperResponse.class;
            }
        };
        Converter<ResponseBody, ?> gsonConverter = factory
                .responseBodyConverter(wrappedType, annotations, retrofit);
        return new WrapperResponseBodyConverter(gsonConverter);
    }
}

and

public class WrapperResponseBodyConverter<T>
        implements Converter<ResponseBody, T> {
    private Converter<ResponseBody, WrapperResponse<T>> converter;

    public WrapperResponseBodyConverter(Converter<ResponseBody,
            WrapperResponse<T>> converter) {
        this.converter = converter;
    }

    @Override
    public T convert(ResponseBody value) throws IOException {
        WrapperResponse<T> response = converter.convert(value);

            return response.getData().getData();

    }
}

Used in my Retrofit Module (dagger2) to ensure that my Retrofit client unwrap any answer from server using the generic WrapperResponse and, in the end, I can write Retrofit method as

@GET("locations")
Observable<List<Location>> getLocation();

where List is exactly the result I wanted to obtain: a list of objects straight from Retrofit response, that I can further elaborate with RxJava.

Thanks all.

Upvotes: 1

Kingfisher Phuoc
Kingfisher Phuoc

Reputation: 8200

As far as I know, there's no way to do it. For best practice, you should create a Facade layer (maybe an ApiManager class) to manage all your APIs. In that case, you can use map/flatMap to map your ObjectResponse to SingleObject like:

public Observable<List<SingleObject>> getSingleObjects(){
    return ServiceGenerator.getApiMethods().getObjectResponse.map(new Function<ObjectResponse, List<SingleObject>>() {
        @Override
        public List<SingleObject> apply(ObjectResponse response) throws Exception {
            return reponse.getListSingleObjects();
        }
    })
}

Upvotes: 2

Related Questions