4

Using Retrofit for network calls and Koin for dependency injection in an Android app, how to support dynamic url change?

(while using the app, users can switch to another server)

EDIT: network module is declared like this:

fun networkModule(baseUrl: String) = module {

    single<Api> {

        Retrofit.Builder()
                .baseUrl(baseUrl) 
                .client(OkHttpClient.Builder().readTimeout(30, TimeUnit.SECONDS)
                        .connectTimeout(30, TimeUnit.SECONDS)
                        .writeTimeout(30, TimeUnit.SECONDS)
                        .build())
                .build().create(Api::class.java)
    }

I am starting Koin in the Aplication class onCreate like this:

 startKoin {

        if (BuildConfig.DEBUG) AndroidLogger() else EmptyLogger()

        androidContext(this@App)

        modules(listOf(networkModule(TEST_API_BASE_URL), storageModule, integrationsModule, appModule))
    }
daneejela
  • 13,081
  • 7
  • 38
  • 50
  • 1
    It will be difficult for anyone to help you without a [mcve] showing your Koin setup and where you are plugging in Retrofit. You need to create a new `Retrofit` instance for the new base URL. How you get that `Retrofit` instance to the code that needs it depends on what needs it and how those things themselves are created. You might be able to use [a scope](https://insert-koin.io/docs/2.0/documentation/reference/index.html#_using_scopes) to force a fresh `Retrofit` instance by closing the scope when the URL changes. – CommonsWare Oct 12 '19 at 20:46
  • You are absolutely right...I've included the relevant part of code.. – daneejela Oct 12 '19 at 20:56
  • 2
    If not scopes, then possibly [unloading](https://insert-koin.io/docs/2.0/documentation/reference/index.html#_unloading_modules) and [reloading](https://insert-koin.io/docs/2.0/documentation/reference/index.html#_loading_modules_after_startkoin) that module will do what you want. – CommonsWare Oct 12 '19 at 20:59
  • 1
    You can overload the URL using Url annotation public interface APIService { @GET Call getUsers(@Url String url); } – Dinesh Oct 12 '19 at 21:04

2 Answers2

2

I faced the same problem recently. The most convenient way is to use a Interceptor to change the baseUrl dynamically.

class HostSelectionInterceptor(defaultHost: String? = null, defaultPort: Int? = null) : Interceptor {
    @Volatile var host: String? = null
    @Volatile var port: Int? = null

    init {
        host = defaultHost
        port = defaultPort
    }

    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
        var request = chain.request()

        this.host?.let {host->
            val urlBuilder = request.url().newBuilder()

            urlBuilder.host(host)

            this.port?.let {
                urlBuilder.port(it)
            }
            request = request.newBuilder().url(urlBuilder.build()).build()
        }

        return chain.proceed(request)
    }
}

Initialize it with your default url.

single { HostSelectionInterceptor(HttpUrl.parse(AppModuleProperties.baseUrl)?.host()) }
single { createOkHttpClient(interceptors = listOf(get<HostSelectionInterceptor>()))}

And add this interceptor when creating your OkHttpClient.

val builder = OkHttpClient().newBuilder()    
interceptors?.forEach { builder.addInterceptor(it) }

To change the url you only have to update the interceptors member.

fun baseUrlChanged(baseUrl: String) {
    val hostSelectionInterceptor = get<HostSelectionInterceptor>()
    hostSelectionInterceptor.host = baseUrl
}
woodii
  • 763
  • 7
  • 23
  • Hi sir, thanks for your reply. I'm pretty much new using koin. Where did you declare and use "baseUrlChanged(baseUrl: String)"? – Kevin Perez Feb 08 '20 at 01:08
  • Hey! In my case I use this to change between different test environments. For this my app has a selection in the navigation-drawer where I can select the desired backend. On selection I call this function to set the host in my interceptor. And each new request will be altered to use the correct host. – woodii Feb 13 '20 at 09:44
  • @woodii where did you actually declare `baseUrlChanged` though? Looks like some scope function? – user1795832 Jun 22 '20 at 16:58
  • The best place to put this function is where you can change your url. Your Navigationdrawer, DebugActivity whatever. Just make sure you can access the injected Interceptor. – woodii Jun 25 '20 at 06:51
2

I've tried with Koin loading/unloading modules..and for a short period of time it worked, but later, after a minimal change I wasn't able to make it reload again.

At the end, I solved it with wrapper object:

class DynamicRetrofit(private val gson: Gson) {

private fun buildClient() = OkHttpClient.Builder()
        .build()

private var baseUrl = "https://etc..." //default url

private fun buildApi() = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(GsonConverterFactory.create(gson))
        .client(buildClient())
        .build().create(MyApi::class.java)

var api: MyApi = buildApi()
    private set

fun setUrl(url: String) {
    if (baseUrl != url)
        baseUrl = url

    api = buildApi()
}}

I declare it in within Koin module like this:

  single<DynamicRetrofit>()
    {
        DynamicRetrofit(get(), get())
    }

and use it in pretty standard way:

dynamicRetrofit.api.makeSomeRequest()

It was good solution for my case since I change baseUrl very rarely. If you need to make often and parallel calls to two different servers it will probably be inefficient since you this will recreate HTTP client often.

daneejela
  • 13,081
  • 7
  • 38
  • 50