Detailed explanation of Android project multi server interface adaptation of ultra simple

  • 2021-12-04 19:59:16
  • OfStack

Status quo

If the Android project is a multi-server interface, what should I do?

Method 1: The server address is placed in Header

The server address is placed in the interface Header, and then the interceptor dynamically modifies the request address. In addition to the interface of the default server, it is a bit troublesome to add an Header. It doesn't look cool or concise.


interface ApiHeaderCase {
  /************************** server A ****************************/
  @Headers("host:$SERVER_HOST_A")
  @GET("user/loginWithScanCode")
  fun aMethod1(@Query("id") id: Int): Observable<ResponseBody>

  /************************** server B ****************************/
  @Headers("host:$SERVER_HOST_B")
  @GET("user/loginWithScanCode")
  fun bMethod1(@Query("id") id: Int): Observable<ResponseBody>
}

Method 2: Multiple sets of service classes are instantiated into multiple objects, and the interface attribution service is accurately found

Define multiple classes, and each class defines a set of service interfaces. Then instantiate them into multiple objects, and then use the exact objects to call the interface. This method is the most efficient, but during development, it may not be possible to quickly know which service the interface belongs to, and it is necessary to check the code to know accurately, which can be said to be less code prompt ability.


interface ApiA {
  @GET("user/loginWithScanCode")
  fun methodA(@Query("id") id: Int): Observable<ResponseBody>
}

interface ApiB {
  @GET("user/loginWithScanCode")
  fun methodB(@Query("id") id: Int): Observable<ResponseBody>
}

Method 3: Write it all from 1, instantiate it into multiple objects, and call the method accurately

Write all interfaces in one class, and then instantiate them into multiple objects according to the service address. Then call the method accurately. In order to ensure the accurate call of the method, each interface can be prefixed with a service name to reduce the problem of wrong method adjustment.


interface ApiAllInOne {
  /************************** server A ****************************/
  @GET("user/loginWithScanCode")
  fun aMethod1(@Query("id") id: Int): Observable<ResponseBody>

  /************************** server B ****************************/
  @GET("user/loginWithScanCode")
  fun bMethod1(@Query("id") id: Int): Observable<ResponseBody>
}

const val SERVER_HOST_A = "https://www.a.com/"
const val SERVER_HOST_B = "https://www.b.com/"
fun getApi(retrofit: Retrofit, host: String): ApiAllInOne {
  return retrofit.newBuilder()
      .baseUrl(host).build()
      .create(ApiAllInOne::class.java)
}

fun showNomalUseCase(retrofit: Retrofit) {
  val apiA = getApi(retrofit, SERVER_HOST_A)//save as single instance for repeated usage
  apiA.aMethod1(1).subscribe()
  apiA.bMethod1(1).subscribe()//invalid usage, but no compile error

  val apiB = getApi(retrofit, SERVER_HOST_B)
  apiB.bMethod1(1).subscribe()
  apiB.aMethod1(1).subscribe()//invalid usage, but no compile error
}

Is there an easier way?

Of course there is, and it is extremely convenient!

Defining an interface

(Recommended) Define all interfaces in one KT file for easy lookup and maintenance.


interface ApiHolder : ApiA, ApiB

  @BaseUrl("https://www.a.com/")
  interface ApiA {
    @GET("user/loginWithScanCode")
    fun methodA(@Query("id") id: Int): Observable<ResponseBody>
  }

  @BaseUrl("https://www.b.com/")
  interface ApiB {
    @GET("user/loginWithScanCode")
    fun methodB(@Query("id") id: Int): Observable<ResponseBody>
  }

Building tool class

1 generally need a tool class, which is convenient to configure interceptors and so on. If there is no custom requirement, it can also be instantiated directly.

You can override the invokeApi method and set threads globally for each Observable.


class ApiUtil : ApiHolderUtil<ApiHolder>(ApiHolder::class) {
  companion object {
    val apiUtil = ApiUtil()
    val api = apiUtil.api
  }

  override fun invokeApi(api: Any, method: Method, args: Array<*>?): Any {
    val observable = super.invokeApi(api, method, args) as Observable<*>
    return observable.subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
  }
}

Dynamic update service address

Service addresses can also be dynamically updated, such as switching between test services and official services.


//update api baseUrl when needed
  apiUtil.updateApi(ApiA::class, https://www.a2.com/)

Calling interface


api.methodA(1).subscribe()
  api.methodB(1).subscribe()

Introducing dependency


dependencies {
  implementation 'com.github.DonaldDu:ApiHolder:x.x.x'//JitPack version
}

Three-party library used in this project

OkHttp3 Retrofit2 rxjava3 (can be modified to rxjava2)

api 'com.squareup.okhttp3:okhttp:4.7.2'
  api "com.squareup.retrofit2:retrofit:2.9.0"
  api "com.squareup.retrofit2:converter-gson:2.9.0"
  api "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
  api 'io.reactivex.rxjava3:rxandroid:3.0.0'

Other notes

rxjava3 - > rxjava2

It can be adjusted to rxjava2 as needed. It is recommended to use the latest one.


// Rewrite ApiHolderUtil The following method, RxJava3CallAdapterFactory ->RxJava2CallAdapterFactory That's enough. 
  protected open fun getRetrofit(client: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .validateEagerly(validateEagerly)
        .addConverterFactory(getGsonConverterFactory())
        .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
        .baseUrl("http://www.demo.com/")
        .client(client)
        .build()
  }

Timeout

You can set different timeouts for each set of services


interface ApiA {
  @GET("user/loginWithScanCode")
  fun methodA(@Query("id") id: Int): Observable<ResponseBody>
}

interface ApiB {
  @GET("user/loginWithScanCode")
  fun methodB(@Query("id") id: Int): Observable<ResponseBody>
}

0

Related articles: