Set dynamic base url using Retrofit 2.0 and Dagger 2

AndroidRetrofit2Dagger 2Dagger Hilt

Android Problem Overview


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?

Android Solutions


Solution 1 - Android

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);

Solution 2 - Android

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

Solution 3 - Android

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()

Solution 4 - Android

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> //

Solution 5 - Android

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);
    }
}

Solution 6 - Android

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();

Solution 7 - Android

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

Solution 8 - Android

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();
        }
    }

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRenjithView Question on Stackoverflow
Solution 1 - AndroidEpicPandaForceView Answer on Stackoverflow
Solution 2 - AndroidJaydev MehtaView Answer on Stackoverflow
Solution 3 - AndroidNandhakumar AppusamyView Answer on Stackoverflow
Solution 4 - Androidganeshraj020794View Answer on Stackoverflow
Solution 5 - AndroidindraView Answer on Stackoverflow
Solution 6 - AndroidyoAlex5View Answer on Stackoverflow
Solution 7 - AndroidOtieno RowlandView Answer on Stackoverflow
Solution 8 - AndroidZeeshan AkhtarView Answer on Stackoverflow