好久之前用 RxJava 搭配 Retrofit 抽过一个网路请求框架《RxJava + Retrofit 应用整理》,现在再来个 Kotlin 版本的 ~
一、框架搭建
网路请求框架基于以下版本:
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
// FastJson
implementation 'com.alibaba:fastjson:1.2.74'
// RXJava
implementation 'io.reactivex.rxjava2:rxjava:2.1.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.6.2"
implementation "com.squareup.retrofit2:converter-gson:2.6.2"
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.2.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"
主体类,除一些初始化操作外,也增加了如下内容:
1、在请求体中增加默认参数
2、自定义打印请求和响应信息(之前采用的是 okhttp3:logging-interceptor,有些适配问题,也不够灵活)
import android.text.TextUtils
import okhttp3.*
import okio.Buffer
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset
import java.util.*
class RetrofitHelper private constructor() {
private val mRetrofit: Retrofit
private val mAppInterface: AppInterface
companion object {
@JvmStatic
val instance: RetrofitHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RetrofitHelper()
}
private val CONTENT_TYPE_JSON: MediaType? = MediaType.parse("application/json")
const val BASE_URL: String = "https://xxx.com/"
}
init {
val okHttpClientBuilder = OkHttpClient.Builder()
setInterceptor(okHttpClientBuilder)
mRetrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create()) // 添加 gson 关联
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 添加 Rxjava 关联
.client(okHttpClientBuilder.build()) // 设置 OkHttpClient
.build()
mAppInterface = mRetrofit.create(AppInterface::class.java)
}
// 设置拦截器
private fun setInterceptor(okHttpClientBuilder: OkHttpClient.Builder) {
val interceptor = Interceptor { chain ->
var request: Request = chain.request()
if ("POST" == request.method()) {
val body = request.body()
// 适配 form 表单类型提交
if (body is FormBody) {
val builder = FormBody.Builder()
// 添加传入参数
for (i in 0 until body.size()) {
builder.add(body.encodedName(i), body.encodedValue(i))
}
// 追加默认参数
builder.add("time", System.currentTimeMillis().toString())
builder.add("version", DeviceUtil.getAppVersionName())
// 构造新的请求体
request = request.newBuilder().post(builder.build()).build()
} else {
val defaultParamMaps = HashMap<String, Any?>()
val bodyString = bodyToString(body)
if (!TextUtils.isEmpty(bodyString)) {
val paramMaps = JSON.parse(bodyString) as Map<*, *>
paramMaps.forEach {
defaultParamMaps[it.key.toString()] = it.value
}
}
defaultParamMaps["time"] = System.currentTimeMillis()
defaultParamMaps["version"] = DeviceUtil.getAppVersionName()
// 默认json类型提交
val requestBody = RequestBody.create(CONTENT_TYPE_JSON, JSON.toJSONString(defaultParamMaps))
request = request.newBuilder().post(requestBody).build()
}
}
logRequestInfo(request)
val response = chain.proceed(request)
logResponseInfo(response)
response
}
okHttpClientBuilder.addInterceptor(interceptor)
}
// 打印请求数据
private fun logRequestInfo(request: Request) {
if (!DebugUtil.IS_DEBUG) {
return
}
val log = StringBuilder()
log.append("\n \n \n>>>>>>>>>>>>>>>>>>>>>> Http-Request >>>>>>>>>>>>>>>>>>>>>>")
log.append("\n>>> ")
log.append("\n>>> Content-Type = ${request.body()?.contentType()}")
log.append("\n>>> $request")
log.append("\n>>> ${bodyToString(request.body())}")
log.append("\n>>> ")
log.append("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n ")
LogUtil.http(log.toString().replace(", ", "\n>>> "))
}
// 打印响应数据
private fun logResponseInfo(response: Response) {
if (!DebugUtil.IS_DEBUG) {
return
}
val charset: Charset
charset = Charset.forName("UTF-8")
val responseBody = response.peekBody(Long.MAX_VALUE)
val jsonReader: Reader = InputStreamReader(responseBody.byteStream(), charset)
val reader = BufferedReader(jsonReader)
val log = java.lang.StringBuilder()
log.append("\n \n \n>>>>>>>>>>>>>>>>>>>>>> Http-Response >>>>>>>>>>>>>>>>>>>>>>")
log.append("\n>>> ")
log.append("\n>>> url=${response.request().url()}")
var line: String? = reader.readLine()
while (line != null) {
log.append("\n>>> $line")
line = reader.readLine()
}
log.append("\n>>> ")
log.append("\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n ")
LogUtil.http(log.toString())
}
private fun bodyToString(request: RequestBody?): String? {
if (request == null) {
return ""
} else {
val buffer = Buffer()
request.writeTo(buffer)
return buffer.readUtf8()
}
}
// ------------------------------------------------------------
// TODO 如果有其它服务器地址,可以在mRetrofit.create前修改baseUrl
// mRetrofit.newBuilder().baseUrl(AppInterface.BASE_URL).build()
fun getAppInterface(): AppInterface {
return mAppInterface
}
}
Api 接口,retrofit 会帮忙生成代码的:
import io.reactivex.Observable
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.*
interface AppInterface {
/**
* 采用 @Url 覆盖 baseUrl
*/
@GET
fun getXxx(@Url url: String?): Call<ResponseBean<Any?>>
/**
* content-type application/x-www-form-urlencoded
*/
@FormUrlEncoded
@POST("xxx/xxx/xxx")
fun checkForm(@FieldMap map: Map<String, @JvmSuppressWildcards Any?>?): Observable<ResponseBean<String>>
@POST("xxx/xxx/xxx")
fun checkInfo(@Body xxxRequestBean: XxxRequestBean?): Observable<ResponseBean<String>>
/**
* 文件下载
* 采用 @Url 覆盖 baseUrl
*/
@GET
fun downloadFileDynamicRepos(@Url fileUrl: String): Call<ResponseBody>
/**
* 多文件下载
* 采用 @Url 覆盖 baseUrl
*/
@Streaming
@GET
fun downloadFileDynamicRepos(@Url fileUrlList: List<String>): Call<ResponseBody>
}
不知怎的,就是很喜欢用 fastjson 的注解,然后在代码混淆规则中,增加忽略被注解字段的混淆规则,妈妈再也不用担心开发忘记加 bean 类的混淆了:
import com.alibaba.fastjson.annotation.JSONField;
public class ResponseBean<T> {
@JSONField(name = "code")
public int code;
@JSONField(name = "msg")
public String msg;
@JSONField(name = "data")
public T data;
}
实际开发中,后台有统一格式的返回 bean,比如当 code=200 时能成功获取到所需数据,其他 code 则要有对应的情形提示或者操作,我们不可能每次都去写这些判断,所以要抽取统一的响应处理逻辑:
import io.reactivex.Observer
import io.reactivex.disposables.Disposable
interface ResponseObserver<T> : Observer<ResponseBean<T>> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: ResponseBean<T>) {
if (200 == t.code) {
onResponseSuccess(t.data)
} else {
onResponseError(t.msg)
}
}
fun onResponseError(msg: String?) {
ToastUtil.showLong(msg)
}
fun onResponseSuccess(data: T?)
override fun onComplete() {
}
override fun onError(e: Throwable) {
ToastUtil.showLong(e.toString())
onResponseError(e.toString())
}
}
通过 Kotlin 的扩展功能,可以把一些通用操作抽取出来,使用时更为简洁:
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
fun <T> Observable<ResponseBean<T>>.defaultHttpRequest(observer: Observer<ResponseBean<T>>) {
this
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer)
}
fun RetrofitHelper.Companion.getDefaultAppInterface(): AppInterface {
return this.instance.getAppInterface()
}
使用示例:
RetrofitHelper.instance.getAppInterface()
.checkInfo(xxxRequestBean)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : ResponseObserver<String> {
override fun onResponseSuccess(data: String?) {
... ...
}
})
RetrofitHelper.instance.getAppInterface()
.downloadFileDynamicRepos(fileUrl)
.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
if (response.isSuccessful && response.body() != null) {
response.body()?.let {
val bytes = it.bytes()
... ...
}
} else {
... ...
}
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
... ...
}
})
RetrofitHelper.instance.getAppInterface()
.getXxx("https://xxx/xxx/xxx")
.enqueue(object : Callback<ResponseBean<Any?>> {
override fun onResponse(call: Call<ResponseBean<Any?>>, response: Response<ResponseBean<Any?>>) {
if (response.isSuccessful && response.body() != null) {
response.body()?.let {
val responseMap = it.data as Map<*, *>
... ...
}
}
}
override fun onFailure(call: Call<ResponseBean<Any?>>, t: Throwable) {
... ...
}
})
没什么,就是觉得很想把 “.instance.” 抽出来,闲的 ~
RetrofitHelper.getDefaultAppInterface()
.checkInfo(xxxRequestBean)
.defaultHttpRequest(object : ResponseObserver<String> {
override fun onResponseSuccess(data: String?) {
... ...
}
})
代码混淆:
### Retrofit + RxJava + OkHttp
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
-dontwarn okhttp3.logging.**
-keep class okhttp3.internal.**{*;}
-dontwarn okio.**
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
rx.internal.util.atomic.LinkedQueueNode producerNode;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
-dontwarn okhttp3.**
-dontwarn sorg.codehaus.mojo.animal_sniffer.**
-dontwarn org.codehaus.**
-dontwarn java.nio.**
-dontwarn java.lang.invoke.**
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class com.google.gson.** {*;}
-keep class com.google.**{*;}
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.examples.android.model.** { *; }
-keep class com.google.gson.stream.** { *; }
-keepattributes EnclosingMethod
后面有机会的话,想结合 Jetpack 和 Kotlin 协程弄一些不一样的东东 ~
参考文章:
1、扩展 - Kotlin 语言中文站