Mobile Development 20 min read

Dynamic Base URL Switching in Retrofit Using Interceptor and Custom Annotations

This article explains the limitations of the default Retrofit baseUrl configuration, reviews three existing solutions, and presents a comprehensive interceptor‑based approach that leverages custom @BaseUrl annotations to dynamically switch base URLs at runtime while handling @Url full‑path parameters correctly.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Dynamic Base URL Switching in Retrofit Using Interceptor and Custom Annotations

Introduction

Retrofit sets the baseUrl when the instance is created and does not allow direct modification later, which becomes problematic when an application needs to switch between multiple servers, such as aggregating data from different platforms or targeting region‑specific endpoints.

Existing Solutions

Multiple Retrofit Instances

Creating a new Retrofit object with a different baseUrl using Retrofit#newBuilder() works but wastes resources because each instance holds its own OkHttp client and converters.

val otherRetrofit = retrofit.newBuilder()
  .baseUrl("xxxxx")
  .build()
val api = otherRetrofit.create
()

@Url Annotation

The official @Url annotation lets a method parameter replace the path part of the request. Supplying a full URL overrides the original baseUrl , but it requires adding a URL parameter with a default value to every request, which is cumbersome and error‑prone.

var globalBaseUrl = ""
interface Api {
  @Post @JvmOverloads
  suspend fun request1(@Url url: String = globalBaseUrl + "/aaa/bbb"): AipResponse
@Post @JvmOverloads
  suspend fun request2(@Url url: String = globalBaseUrl + "/ccc/ddd"): AipResponse
}

globalBaseUrl = "http://www.xxxxx.com"
api.request1()

Interceptor + Header (RetrofitUrlManager)

Using an OkHttp interceptor to replace the baseUrl is more efficient. Libraries such as RetrofitUrlManager add a header (e.g., Domain-Name ) to indicate which domain to use, or set a global domain for all requests.

okHttpClient = RetrofitUrlManager.getInstance()
    .with(OkHttpClient.Builder())
    .build()

RetrofitUrlManager.getInstance().setGlobalDomain("your BaseUrl")

RetrofitUrlManager.getInstance().putDomain("douban", "https://api.douban.com")

However, this approach still replaces a full‑path URL supplied via @Url , causing upload requests to fail when the server returns a dynamic upload address.

Design of a New Solution

Reading Annotation Information in the Interceptor

Retrofit creates the OkHttp Request inside a dynamic proxy. The proxy stores an Invocation object as a tag on the request, which contains the reflected Method and its arguments. By retrieving this tag in the interceptor, we can inspect both method‑level and class‑level annotations.

override fun intercept(chain: Chain): Response {
  val request = chain.request()
  val invocation = request.tag(Invocation::class.java)
  val method = invocation?.method() ?: return chain.proceed(request)
  // further logic …
  return chain.proceed(request)
}

Custom @BaseUrl Annotation

A unified annotation @BaseUrl is introduced. It can be placed on a class or a method and supports two optional parameters: value for a static URL and key for a dynamic URL that is looked up in a ConcurrentHashMap<String, String> called dynamicBaseUrls .

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class BaseUrl(val value: String = "", val key: String = "")

Resolution Rules (Priority Order)

If a parameter annotated with @Url contains a full HTTP/HTTPS URL, use it directly.

Look for a method‑level @BaseUrl with a non‑empty key and resolve it from dynamicBaseUrls .

If not found, check the class‑level @BaseUrl with a key .

Next, use a method‑level @BaseUrl with a static value .

Then, a class‑level @BaseUrl with a static value .

Fallback to the globally configured globalBaseUrl .

Finally, keep the original Retrofit baseUrl .

Implementation Highlights

The interceptor extracts the index of the @Url parameter, checks whether the argument is a valid full URL, and returns early if so. Otherwise it builds a UrlsConfig object (cached per Method ) containing the static URL, dynamic keys, and the annotation index.

private val urlsConfigCache = ConcurrentHashMap
()

data class UrlsConfig(
  val apiBaseUrl: String?,
  val methodUrlKey: String?,
  val clazzUrlKey: String?,
  val urlAnnotationIndex: Int,
)

When a new base URL is resolved, the interceptor reconstructs the request URL by replacing the scheme, host, port, and merging the original path segments with the new base URL’s path segments.

val newFullUrl = request.url.newBuilder()
  .scheme(newBaseUrl.scheme)
  .host(newBaseUrl.host)
  .port(newBaseUrl.port)
  .apply {
    repeat(request.url.pathSize) { removePathSegment(0) }
    (newBaseUrl.encodedPathSegments + request.url.encodedPathSegments).forEach { addEncodedPathSegment(it) }
  }
  .build()
return chain.proceed(request.newBuilder().url(newFullUrl).build())

Final Library – MultiBaseUrls

The complete implementation is packaged as the MultiBaseUrls library. It can be added via JitPack, enabled on an OkHttpClient.Builder , and used from both Kotlin and Java.

// Gradle (settings.gradle)
dependencyResolutionManagement {
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  repositories {
    mavenCentral()
    maven { url "https://www.jitpack.io" }
  }
}

// Module dependencies
implementation("com.github.DylanCaiCoding:MultiBaseUrls:1.0.0")

Usage example (Kotlin):

val okHttpClient = OkHttpClient.Builder()
    .enableMultiBaseUrls()
    .build()

@BaseUrl("https://api.example.com/")
interface ApiService {
    @GET("/v2/book/{id}")
    fun getBook(@Path("id") id: Int): Observable
}

Dynamic URLs can be changed at runtime:

globalBaseUrl = "https://api.example.com/v2/"
dynamicBaseUrls["url1"] = "https://service1.example.com/"

The library also provides a switch to disable the multi‑base‑url feature and revert to the standard Retrofit behavior.

Conclusion

The article compares three traditional ways of changing Retrofit's baseUrl , points out their drawbacks—especially the inability to correctly handle full‑path URLs passed via @Url —and introduces a robust interceptor‑plus‑annotation solution. By reading Retrofit's internal Invocation tag, the interceptor can decide which URL to apply based on a clear priority hierarchy, cache annotation parsing for performance, and give developers fine‑grained control over both static and dynamic base URLs.

androidKotlinInterceptorAnnotationRetrofitDynamicBaseUrl
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.