91

I'm trying to perform a login action using Retrofit 2.0 using Dagger 2

Here's how I set up Retrofit dependency

@Provides
@Singleton
Retrofit provideRetrofit(Gson gson, OkHttpClient client) {
    Retrofit retrofit = new Retrofit.Builder()
                            .addConverterFactory(GsonConverterFactory.create(gson)
                            .client(client)
                            .baseUrl(application.getUrl())
                            .build();
    return retrofit;     
}

Here's the API interface.

interface LoginAPI {
   @GET(relative_path)
   Call<Boolean> logMe();
}

I have three different base urls users can log into. So I can't set a static url while setting up Retrofit dependency. I created a setUrl() and getUrl() methods on Application class. Upon user login, I set the url onto Application before invoking the API call.

I use lazy injection for retrofit like this

Lazy<Retrofit> retrofit

That way, Dagger injects dependency only when I can call

retrofit.get()

This part works well. I got the url set to retrofit dependency. However, the problem arises when the user types in a wrong base url (say, mywifi.domain.com), understands it's the wrong one and changes it(say to mydata.domain.com). Since Dagger already created the dependency for retrofit, it won't do again. So I have to reopen the app and type in the correct url.

I read different posts for setting up dynamic urls on Retrofit using Dagger. Nothing really worked out well in my case. Do I miss anything?

Bolt UIX
  • 5,988
  • 6
  • 31
  • 58
Renjith
  • 3,457
  • 5
  • 46
  • 67

8 Answers8

72

Support for this use-case was removed in Retrofit2. The recommendation is to use an OkHttp interceptor instead.

HostSelectionInterceptor made by swankjesse

import java.io.IOException;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;

/** An interceptor that allows runtime changes to the URL hostname. */
public final class HostSelectionInterceptor implements Interceptor {
  private volatile String host;

  public void setHost(String host) {
    this.host = host;
  }

  @Override public okhttp3.Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    String host = this.host;
    if (host != null) {
      //HttpUrl newUrl = request.url().newBuilder()
      //    .host(host)
      //    .build();
      HttpUrl newUrl = HttpUrl.parse(host);
      request = request.newBuilder()
          .url(newUrl)
          .build();
    }
    return chain.proceed(request);
  }

  public static void main(String[] args) throws Exception {
    HostSelectionInterceptor interceptor = new HostSelectionInterceptor();

    OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .addInterceptor(interceptor)
        .build();

    Request request = new Request.Builder()
        .url("http://www.coca-cola.com/robots.txt")
        .build();

    okhttp3.Call call1 = okHttpClient.newCall(request);
    okhttp3.Response response1 = call1.execute();
    System.out.println("RESPONSE FROM: " + response1.request().url());
    System.out.println(response1.body().string());

    interceptor.setHost("www.pepsi.com");

    okhttp3.Call call2 = okHttpClient.newCall(request);
    okhttp3.Response response2 = call2.execute();
    System.out.println("RESPONSE FROM: " + response2.request().url());
    System.out.println(response2.body().string());
  }
}

Or you can either replace your Retrofit instance (and possibly store the instance in a RetrofitHolder in which you can modify the instance itself, and provide the holder through Dagger)...

public class RetrofitHolder {
   Retrofit retrofit;

   //getter, setter
}

Or re-use your current Retrofit instance and hack the new URL in with reflection, because screw the rules. Retrofit has a baseUrl parameter which is private final, therefore you can access it only with reflection.

Field field = Retrofit.class.getDeclaredField("baseUrl");
field.setAccessible(true);
okhttp3.HttpUrl newHttpUrl = HttpUrl.parse(newUrl);
field.set(retrofit, newHttpUrl);
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Thanks for the reply! I had a gray idea on the second option you've mentioned. I reckon second option is much neater since it doesn't have to intercept every request sent out from the app. Which option do you think is recommendable? – Renjith Apr 08 '16 at 13:35
  • 1
    The official recommendation is the first. A possible workaround is the second. The third one is a hideous hack, but I love reflection, hah. Honestly, going with what I did with Realm (injecting a `RealmHolder` from a scoped provider), I'd probably do `2`. – EpicPandaForce Apr 08 '16 at 13:46
  • 1
    But if you run this two request at the same time parallel? Will be there error, because two requests will have the same baseUrl in first example? – Igor Kostenko Sep 07 '16 at 11:03
  • @IhorKostenko well, I don't think the access to `RetrofitHolder` is threadsafe. – EpicPandaForce Sep 07 '16 at 11:05
  • So the best one is to create new instance after changing some base params? – Igor Kostenko Sep 07 '16 at 11:07
  • Well you shouldn't recreate the Retrofit instance too many times, but the whole Retrofit2/OkHttp3 thing works by setting values and then not changing them afterwards ("immutability"), so that makes these "dynamic" use-cases somewhat difficult. – EpicPandaForce Sep 07 '16 at 11:11
  • @EpicPandaForce hi sir, could you help me on this topic `https://stackoverflow.com/q/45519852/1830228` ? Thanks – DolDurma Aug 05 '17 at 08:25
  • Reflection rocks! – huseyin Aug 28 '17 at 12:26
  • 2
    But be sure, that after url replacing you has your path. Example: http://oldurl.com/api/v2/someapi/ You want to change your domain to newurl.com after replacing it will be http://newurl.com/ but as you can see api path is not present here. Best solution as for me: final String BASE_URL = "http://{subdomain}.domain.com/ String url = request.url().toString().replace("{domain}", domain); In your interpceptor: request = request.newBuilder().url(url).build(); – Denis Makovsky Dec 03 '18 at 11:30
  • Is this still the best approach? – user1795832 Feb 21 '20 at 20:52
  • I would use the `RetrofitHolder` variant, probably – EpicPandaForce Feb 21 '20 at 21:05
69

Retrofit2 library comes with a @Url annotation. You can override baseUrl like this:

API interface:

public interface UserService {  
    @GET
    public Call<ResponseBody> profilePicture(@Url String url);
}

And call the API like this:

Retrofit retrofit = Retrofit.Builder()  
    .baseUrl("https://your.api.url/");
    .build();

UserService service = retrofit.create(UserService.class);  
service.profilePicture("https://s3.amazon.com/profile-picture/path");

For more details refer to this link: https://futurestud.io/tutorials/retrofit-2-how-to-use-dynamic-urls-for-requests

Thomas Vos
  • 12,271
  • 5
  • 33
  • 71
Jaydev Mehta
  • 723
  • 5
  • 4
  • 7
    exception: Url cannot be used with GET URL (parameter #1) – TeeTracker May 09 '17 at 20:18
  • 1
    @TeeTracker question is about retrofit + DI – DolDurma Aug 05 '17 at 06:29
  • This is what I was looking for, really handy when you can use a custom url with @Url – stevyhacker Jan 06 '18 at 01:22
  • 5
    @TeeTracker either you can use `@GET(NEW_BASE_URL + "/v1/whatever")` or `@GET` and parameter `@Url String url`. You can't use both together. – Micer Jul 22 '18 at 10:14
  • I think it will work with GET method only, but what about POST method ? – Pankaj Gadhiya Nov 11 '19 at 13:15
  • 1
    Terrible solution – breakline Nov 22 '19 at 10:32
  • Actually this is by far the best solution! The given solutions on above are problematic, imaging if you have to fetch data from different api's in multiple occasions simultaneously, you would have to call setBase url multiple times back and forth, hence it will lead to possible/terrible bug in your code I have used this approach and it works both for POST and GET method. In my app I used retrofit to sign up users, update/delete email, change password etc on Firebase, and the same time I had to fetch data from different API for presentation. It works perfectly fine! – Junia Montana Apr 01 '20 at 06:57
15

This worked for me in Kotlin

class HostSelectionInterceptor: Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        var request = chain.request()

        val host: String = SharedPreferencesManager.getServeIpAddress()

        val newUrl = request.url().newBuilder()
            .host(host)
            .build()

        request = request.newBuilder()
            .url(newUrl)
            .build()

        return chain.proceed(request)
    }

}

Add the interceptor to OkHttpClient builder

val okHttpClient = OkHttpClient.Builder()
                .addInterceptor(HostSelectionInterceptor())
                .cache(null)
                .build()
Nandhakumar Appusamy
  • 1,156
  • 13
  • 22
10

This might be late but Retrofit allows you to use dynamic URLs while making the network call itself using @Url annotation. I am also using Dagger2 to inject the Retrofit instance in my repositories and this solution is working fine for me.

This will use the base url

provided by you while creating the instance of Retrofit.

@GET("/product/123")
fun fetchDataFromNetwork(): Call<Product>

This ignore the base url

and use the url you will be providing this call at run time.

@GET()
fun fetchDataFromNetwork(@Url url : String): Call<Product> //
Community
  • 1
  • 1
ganeshraj020794
  • 188
  • 2
  • 13
  • 2
    This is not working. I get this error `@Url cannot be used with @GET URL (parameter #1)` – Denny Apr 27 '21 at 06:04
6

Thanks to @EpicPandaForce for help. If someone is facing IllegalArgumentException, this is my working code.

public class HostSelectionInterceptor implements Interceptor {
    private volatile String host;

    public void setHost(String host) {
        this.host = HttpUrl.parse(host).host();
    }

    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        String reqUrl = request.url().host();

        String host = this.host;
        if (host != null) {
            HttpUrl newUrl = request.url().newBuilder()
                .host(host)
                .build();
            request = request.newBuilder()
                .url(newUrl)
                .build();
        }
        return chain.proceed(request);
    }
}
indra
  • 99
  • 1
  • 6
3

Dynamic url using Retrofit 2 and Dagger 2

You are able to instantiate new object using un-scoped provide method.

@Provides
LoginAPI provideAPI(Gson gson, OkHttpClient client, BaseUrlHolder baseUrlHolder) {
    Retrofit retrofit = new Retrofit.Builder().addConverterFactory(GsonConverterFactory.create(gson)
                        .client(client)
                        .baseUrl(baseUrlHolder.get())
                        .build();
    return retrofit.create(LoginAPI.class);     
}

@AppScope
@Provides
BaseUrlHolder provideBaseUrlHolder() {
    return new BaseUrlHolder("https://www.default.com")
}

public class BaseUrlHolder {
    public String baseUrl;

    public BaseUrlHolder(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    public String getBaseUrl() {
        return baseUrl;
    }

    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }
}

Now you can change base url via getting baseUrlHolder from the component

App.appComponent.getBaseUrlHolder().set("https://www.changed.com");
this.loginApi = App.appComponent.getLoginApi();
yoAlex5
  • 29,217
  • 8
  • 193
  • 205
3

For latest Retrofit library, you can simply use singleton instance and change it with retrofitInstance.newBuilder().baseUrl(newUrl). No need to create another instance.

Otieno Rowland
  • 2,182
  • 1
  • 26
  • 34
  • Perhaps, you could use scopes, not sure, but I think, that is one way to wor around it with Dagger. – Otieno Rowland Feb 21 '20 at 22:43
  • `newBuilder().baseUrl(newUrl)` won't change a base url for the current instance. It let you create new builder with predefined values from the current instance but in the end you have to use `build()` method to create an instance. – MDikkii Jun 29 '22 at 10:46
3

Please look into my workaround for Dagger dynamic URL.

Step1: Create an Interceptor

import android.util.Patterns;

import com.nfs.ascent.mdaas.repo.network.ApiConfig;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class DomainURLInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();

        String requestUrl = original.url().toString();
        String PROTOCOL = "(?i:http|https|rtsp)://";
        String newURL = requestUrl.replaceFirst(PROTOCOL, "")
                .replaceFirst(Patterns.DOMAIN_NAME.toString(), "");
        newURL = validateBackSlash(newURL) ? ApiConfig.BASE_URL.concat(newURL) : newURL.replaceFirst("/", ApiConfig.BASE_URL);
        original = original.newBuilder()
                .url(newURL)
                .build();

        return chain.proceed(original);
    }

    private boolean validateBackSlash(String str) {
        if (!str.substring(str.length() - 1).equals("/")) {
            return true;
        }
        return false;
    }

}

Step 2:

add your newly created interceptor in your module

    @Provides
    @Singlton
    DomainURLInterceptor getChangeURLInterceptor() {
        return new DomainURLInterceptor();
    }

step 3: add interceptor into list of HttpClient interceptors

    @Provides
    @Singlton
    OkHttpClient provideHttpClient() {
        return new OkHttpClient.Builder()
                .addInterceptor(getChangeURLInterceptor())
                .readTimeout(ApiConfig.API_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                .connectTimeout(ApiConfig.API_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
                .build();
    }

step 4:

    @Provides
    @Singlton
    Retrofit provideRetrofit() {
        return new Retrofit.Builder()
                .baseUrl(ApiConfig.BASE_URL) // this is default URl,
                .addConverterFactory(provideConverterFactory())
                .client(provideHttpClient())
                .build();
    }

Note: if the user has to change the Base URL from settings, remember to validate the newly created URL with below method:

    public final static boolean isValidUrl(CharSequence target) {
        if (target == null) {
            return false;
        } else {
            return Patterns.WEB_URL.matcher(target).matches();
        }
    }
Dharman
  • 30,962
  • 25
  • 85
  • 135
Zeeshan Akhtar
  • 441
  • 1
  • 4
  • 9