Search code examples
androidretrofit

Adding duplicate parameters with Retrofit v1.9.0


There's a similar question asked here, but my case is a bit different.

I'm trying to make a request similar to the following:

http://www.example.com/abc?foo=def&foo=ghi&foo=jkl&bar=xyz

I have two issues that are making things difficult. First, the repeated parameter (setting values for "foo" multiple times) is preventing the use of QueryMap (I don't have the option to pass the values in the query string differently, like as an array). Second, the query parameters I'm using are sort of dynamic, so I can't really use Query and supply it a list of values for a given parameter name since I won't know the parameter names until I am making the request.

The code I'm trying to upgrade from is using an older version of Retrofit, but somehow it has a concept of a QueryList which took a List of NameValuePairs to pass in query parameters as the name (and their values as the value) and allowing duplicate parameters. I don't see a reference to retrofit.http.QueryList anywhere in Retrofit's source code history or on the web, so I'm not sure if this was a custom addition at the time. In any case, I'm trying to find the best way to replicate that functionality in the latest version so any help would be appreciated!


Solution

  • To follow-up, I was able to resolve this by using QueryMap and a bit of a hack. Basically I turn my List of NameValuePairs into a HashMap where I check if I already have the key, and if I do I append the new value for the same key to the old value. So essentially (key, value) would turn into (key, value&key=value2). This way when the query string is constructed, I'll have key=value&key=value2 as desired. In order for that to work, I need to handle the value encoding myself so that the additional ampersands and equal signs that I'm including in the value don't get encoded.

    So the HashMap is constructed from the List like this:

    public static HashMap<String, String> getPathMap(List<NameValuePair> params) {
        HashMap<String, String> paramMap = new HashMap<>();
        String paramValue;
    
        for (NameValuePair paramPair : params) {
            if (!TextUtils.isEmpty(paramPair.getName())) {
                try {
                    if (paramMap.containsKey(paramPair.getName())) {
                        // Add the duplicate key and new value onto the previous value
                        // so (key, value) will now look like (key, value&key=value2)
                        // which is a hack to work with Retrofit's QueryMap
                        paramValue = paramMap.get(paramPair.getName());
                        paramValue += "&" + paramPair.getName() + "=" + URLEncoder.encode(String.valueOf(paramPair.getValue()), "UTF-8");
                    } else {
                        // This is the first value, so directly map it
                        paramValue = URLEncoder.encode(String.valueOf(paramPair.getValue()), "UTF-8");
                    }
                    paramMap.put(paramPair.getName(), paramValue);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
        }
    
        return paramMap;
    }
    

    Then my request looks like this:

    @GET("/api/")
    List<Repo> listRepos(@QueryMap(encodeValues=false) Map<String, String> params);
    

    And my service call looks like this:

    // Get the list of params for the service call
    ArrayList<NameValuePair> paramList = getParams();
    
    // Convert the list into a map and make the call with it
    Map<String, String> params = getPathMap(paramList);
    List<Repo> repos = service.listRepos(params);
    

    I originally tried a solution using Path where I tried to manually construct the query string, but replacement blocks are not allowed in the query string so I went with this QueryMap solution. Hopefully this helps anyone else that runs into the same issue!