Retrofit: How to specify comma-separated parameters in request?
Asked Answered
D

5

18

I'm trying to refactor my code to use Retrofit (from Volley) for some Foursquare API calls but haven't found a proper example that shows how to specify a query parameter that has 2 values separated by a comma.

My base url is the following:

public static final String VENUES_BASE_URL = "https://api.foursquare.com/v2/venues";

The rest of my url is like this:

"?ll=40.7,50.2&limit=50&radius=25000&v=20140909&venuePhotos=1&oauth_token=xxyyxx";

1st implementation for my interface:

public interface Fourquare {
    @GET("/explore?ll={p1},{p2}&limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
    Response getVenues(@Path("p1") String param1,
                   @Path("p2") String param2);

}

And then made the request like this:

RestAdapter restAdapter = new RestAdapter.Builder()
            .setEndpoint(ConfigConstants.VENUES_BASE_URL)
            .build();

    Fourquare fourquare = restAdapter.create(Fourquare.class);
    Response myResponse = fourquare.getVenues("50", "75");

However, the above gave me the following error:

retrofit.RetrofitError: Fourquare.getVenues: URL query string "ll={p1},{p2}&limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx" must not have replace block.

2nd implementation (After looking at some of the SO responses that use Query parameters. NOTE: Once I figure out the ll? parameter call I will have the token as a parameter):

@GET("/explore&limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@Query("ll") String ll,
                      Callback<String> cb);

With the actual call like this:

fourquare.getVenues("50,75", new Callback<String>() {
        @Override
        public void success(String s, Response response) {
            Log.d(TAG, "Successful run!");
        }

        @Override
        public void failure(RetrofitError error) {
            Log.d(TAG, "Failed run!");
        }
    });

With the above implementation the failure() method is always getting called, so there is still something wrong with my code. Can someone please give some suggestions as to the proper way to implement this call? I'm most certain the issue is with the "ll?" parameter.

Update: After turning logging this is the final url I'm getting from Retrofit: https://api.foursquare.com/v2/venues/explore&limit=50&radius=25000&v=20140909&venuePhotos=1&oauth_token=xxyyxx?ll=30.26%2C-97.74

It looks like the Foursquare server does not like the ?ll parameter at the end of the url and it must be explicitly placed right after ../v2/venues/explore as that is working fine when a place the request through the browser.

Any solutions to get around this limitation from the API?

3rd implementation (9/17/14) With colriot suggestion I was able to resolve the 400 response code I was getting with my previous implementation. I'm still having a speed issue with GSON so looking for suggestions on how to fix that. Specifically, my Retrofit implementation is taking longer to display my results compared to Volley so I'm wondering if there is a better way to implement the callback.

Foursquare interface

public interface Fourquare {   
    @GET("/explore?limit=50&radius=25000&v=20140909&venuePhotos=1&oauth_token=xxyyxx")
    void getVenues(@Query("ll") String ll,
                Callback<Object> cb);
}

RestAdapter call

RestAdapter restAdapter = new RestAdapter.Builder()
            .setEndpoint(ConfigConstants.VENUES_BASE_URL)
            .build();

    Foursquare foursquare = restAdapter.create(Foursquare.class);

foursquare.getVenues("30.26,-97.74", new Callback<Object>() {
        @Override
        public void success(Object o, Response response) {
            Log.d(TAG, "Success!");
            // Parse response
            GsonBuilder gsonBuilder = new GsonBuilder();
            Gson gson = gsonBuilder.create();
            JsonParser parser = new JsonParser();
            String response2 = gson.toJson(o);
            JsonObject data = parser.parse(response2).getAsJsonObject();

            // Populate data model
            MetaResponse metaResponse = gson.fromJson(data.get("meta"), MetaResponse.class);
            VenuesExploreResponse myResponse = gson.fromJson(data.get("response"), VenuesExploreResponse.class);                

            // Store results from myResponse in List
        }

        @Override
        public void failure(RetrofitError error) {
            Log.d(TAG, "Failures!");
        }
    });

The current issue with the above callback implementation is that it is taking longer (about 1 second) than with Volley to parse and show the results. The GsonBuilder/Gson/JsonParser block is exactly the same as in my Volley onResponse(String response) method except for that intermediate "response2" object, so most definitely this intermediate/extra step is the bottleneck. I'm looking for suggestions on how to better implement the Gson parsing. If this might fit better as a new/separate question I'll do so.

Donny answered 10/9, 2014 at 2:59 Comment(0)
M
12

So, as we already found out the problem was in ? -> & typo. But one more thing to be mentioned is that Retrofit can accept complex objects as call parameters. Then String.valueOf(object) will be called to convert object into Query/Path param.

In your case you could define custom class like that:

class LatLng {
  private double lat;
  private double lng;

  ...

  @Override public String toString() {
    return String.format("%.1f,%.1f", lat, lng);
  }
}

And refactor your endpoint method like that:

@GET("/explore?limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@Query("ll") LatLng ll, Callback<String> cb);

About parsing the answer:

  • Never create Gson objects right in callback. It's simply too heavyweight. Use the one you provide to RestAdapter.
  • Why do you mix JsonParser & Gson? They are different tools for basically the same problem.
  • Make use of Retrofit's built-in converter mechanism ;)

From words to code:

public class FoursquareResponse<T> {
  private MetaResponse meta;
  private T response;
  // getters
}

Altogether:

@GET("/explore?limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@Query("ll") LatLng ll, Callback<FoursquareResponse<VenuesExploreResponse>> cb);

...

foursquare.getVenues(LatLng.valueOf(30.26, -97.74), new Callback<FoursquareResponse<VenuesExploreResponse>>() {
    @Override 
    public void success(FoursquareResponse<VenuesExploreResponse> r, Response response) {
        MetaResponse metaResponse = r.getMeta;
        VenuesExploreResponse myResponse = r.getResponse();

        // Store results from myResponse in List
    }

    @Override
    public void failure(RetrofitError error) {
        Log.d(TAG, "Failures!");
    }
});
Manchineel answered 18/9, 2014 at 14:7 Comment(4)
Thanks for the suggestion. I'll definitely refactor that. Any suggestion on the Gson parsing part? Should I submit a new question for that?Donny
@Donny sorry, I just haven't noticed 3rd note at the first time.Manchineel
Thanks for the detailed explanation on using Retrofit's built-in Gson parsing capabilities. I took the whole JsonParser/Gson code from an answer to a Gson-specific question I made a while back. I'm definitely doing a deep dive on Retrofit's code.Donny
@Donny welcome! Also if you want to customize parsing parameters, for example API gives you underscored field names but you use camelCase in your code, then just configure custom Gson instance and pass it to RestAdapter.Builder.Manchineel
L
17

If you are using kotlin and you have var items: List<String> that should go as a query param.

Use this method to form up string:

fun itemsString(items: List<String>) = items.joinToString(separator = ",")

Your url query should be like :

@Query(value = "items", encoded = true) String items
Labiodental answered 17/7, 2019 at 6:39 Comment(0)
M
12

So, as we already found out the problem was in ? -> & typo. But one more thing to be mentioned is that Retrofit can accept complex objects as call parameters. Then String.valueOf(object) will be called to convert object into Query/Path param.

In your case you could define custom class like that:

class LatLng {
  private double lat;
  private double lng;

  ...

  @Override public String toString() {
    return String.format("%.1f,%.1f", lat, lng);
  }
}

And refactor your endpoint method like that:

@GET("/explore?limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@Query("ll") LatLng ll, Callback<String> cb);

About parsing the answer:

  • Never create Gson objects right in callback. It's simply too heavyweight. Use the one you provide to RestAdapter.
  • Why do you mix JsonParser & Gson? They are different tools for basically the same problem.
  • Make use of Retrofit's built-in converter mechanism ;)

From words to code:

public class FoursquareResponse<T> {
  private MetaResponse meta;
  private T response;
  // getters
}

Altogether:

@GET("/explore?limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@Query("ll") LatLng ll, Callback<FoursquareResponse<VenuesExploreResponse>> cb);

...

foursquare.getVenues(LatLng.valueOf(30.26, -97.74), new Callback<FoursquareResponse<VenuesExploreResponse>>() {
    @Override 
    public void success(FoursquareResponse<VenuesExploreResponse> r, Response response) {
        MetaResponse metaResponse = r.getMeta;
        VenuesExploreResponse myResponse = r.getResponse();

        // Store results from myResponse in List
    }

    @Override
    public void failure(RetrofitError error) {
        Log.d(TAG, "Failures!");
    }
});
Manchineel answered 18/9, 2014 at 14:7 Comment(4)
Thanks for the suggestion. I'll definitely refactor that. Any suggestion on the Gson parsing part? Should I submit a new question for that?Donny
@Donny sorry, I just haven't noticed 3rd note at the first time.Manchineel
Thanks for the detailed explanation on using Retrofit's built-in Gson parsing capabilities. I took the whole JsonParser/Gson code from an answer to a Gson-specific question I made a while back. I'm definitely doing a deep dive on Retrofit's code.Donny
@Donny welcome! Also if you want to customize parsing parameters, for example API gives you underscored field names but you use camelCase in your code, then just configure custom Gson instance and pass it to RestAdapter.Builder.Manchineel
C
0

Just urlencode your comma separated arguments, comma = %2

Corycorybant answered 10/9, 2014 at 3:8 Comment(6)
@JakeWharton I wonder if the issue could be the order in which Retrofit is ordering the parameters when making the actual http request. Does Retrofit insert the Query parameters right after the base url? Or at the end of the full URL? I'm currently getting a 400 on the code I posted.Donny
It will append dynamic ones to the end of the static query parameters. You can turn on logging to see the full outgoing URL.Sliwa
@JakeWharton I updated my question as per your logging suggestion. The server does not like the ?ll paramter at the end of the URL. Any recommendations on how to get around this limitation?Donny
@Donny replace & after /explore with ?Manchineel
@Manchineel Can't believe I missed that typo. That fixed the 400 response issue. Thanks! Writing an update on my post with one remaining issue in case you want credit for the answer.Donny
Not %2, but %2C — there are always two hexadecimal digits after percent mark.Hyponitrite
A
0

Try to add all Query params (fixed and variable) in valid order. I mean

@GET("/explore")
Response getVenues(@Query("ll") ll, @Query("limit") limit, @Query("radius") radius, @Query("v") v, @Query("venuePhotos") venuePhotos, @Query("oauth_token") oauth_token);

and wrap call with function that have fixed params as constant

Accompanyist answered 18/9, 2014 at 7:19 Comment(1)
I was able to re-solve my 400 code with @Manchineel suggestion, but yes, I was planning to refactor the interface by putting most if not all of the parameters as Query parameters. Thanks for the feedback.Donny
P
0

You can use @EncodedQuery:

@GET("/explore&limit=50&radius=25000&v=20140905&venuePhotos=1&oauth_token=xxyyxx")
void getVenues(@EncodedQuery("ll") LatLng ll, Callback<String> cb);
Pronghorn answered 22/5, 2015 at 14:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.