Android搭建应用框架系列之Retrofit封装

前言

当我们要从零去搭建一个自己的应用框架时 。做为2017Android程序员的我,就会把Kotlin+Retrofit+MVP+RX系列拿的去实战。整体框架模式构思好后,那就得想想大概实现的步骤。说到这里,就得整理下应用大概有哪些东西了。

应用模块总结.png

目前个人能想到的也就这些,这样就有个引导的步骤和思路了。所以写了下面几篇文章

Android搭建应用框架系列之Retrofit封装
Android搭建应用框架系列之MVP封装
Android搭建应用框架系列之RxBus
Android搭建应用框架系列之ORM数据库
Android搭建应用框架系列之Glide
Android搭建应用框架系列之BaseActivity
Android搭建应用框架系列之StatusView

也算自己给自己的的一些总结,具体代码参考GoachFrame-Github

接下来,就先从网络层Retrofit+OkHttp说起,记得以前自己写过一篇Retrofit的博客,学会Retrofit+OkHttp+RxAndroid三剑客的使用,让自己紧跟Android潮流的步伐,但返回的数据没有结合RxJava来使用,所以这里重新来写下。

思路

我们都知道,实现一个Retrofit大概需要下面的几个步骤

  1. 配置一个OkHttp对象
  2. 配置BaseUrl
  3. 需要返回Obserable对象就配置RxJava2CallAdapterFactory
  4. 需要Gson解析就配置GsonConverterFactory

同时一个Retrofit对象最好对应一个BaseUrl。本着封装变化的原则,仔细相想,这里也就OkHttp是变化的,BaseUrl可以通过参数传入,然后RxJavaCallAdapterFactoryGsonConverterFactory直接配置,另外GsonConverterFactory可以传入一个Gson对象,来统一处理返回JSON数据。其中Retrofit创建通过一个单例来实现,自定义的OkHttp对象可在Application里面注入,也可以直接传默认实现的OkHttp对象

OkHttp

  • 先在build.gradle添加依赖库
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.3-2'
implementation 'com.android.support:appcompat-v7:26.+'
implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2::2.3.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.jakewharton.timber:timber:4.5.1'

其中okhttp3:logging库是下面做http请求拦截器使用,TimberLog封装库

  • 创建OkHttp对象的接口IClient
interface IClient {
    fun getClient():OkHttpClient
}

抽象一个getClient方法供外部自己配置。

  • 接下来设置一个默认配置的OkHttpDefaultOkHttpClient
class DefaultOkHttpClient:IClient {
    private var mConnectionTimeOut = Consts.CONNECTION_TIMEOUT
    private var mWriteTimeOut = Consts.CONNECTION_TIMEOUT
    private var mReadTimeOut = Consts.CONNECTION_TIMEOUT
    private var isRetryOnConnectionFailure = Consts.IS_RETRY_ON_CONNECTION_FAILURE
    private var mCookieJar = getCookieJar()
    private var mInterceptors : Array<Interceptor> = emptyArray()
    override fun getClient(): OkHttpClient {
        return OkHttpClient()
                .newBuilder()
                .connectTimeout(mConnectionTimeOut,TimeUnit.SECONDS)
                .writeTimeout(mWriteTimeOut,TimeUnit.SECONDS)
                .readTimeout(mReadTimeOut,TimeUnit.SECONDS)
                .retryOnConnectionFailure(isRetryOnConnectionFailure)
                .cookieJar(mCookieJar)
                .addInterceptors(mInterceptors)
                .build()
    }

    fun getCookieJar():CookieJar{
        return object:CookieJar{
            var mCookieStore:MutableMap<String,MutableList<Cookie>> = mutableMapOf()
            override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
                mCookieStore.put(url.host(),cookies)
            }

            override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
                val cookies = mCookieStore[url.host()]
                return cookies?: mutableListOf()
            }
        }
    }
    fun OkHttpClient.Builder.addInterceptors(mInterceptors : Array<Interceptor>):OkHttpClient.Builder{
        if(mInterceptors.isNotEmpty()){
            mInterceptors.forEach {
                this.addInterceptor(it)
            }
        }
        return this
    }

    fun setConnectionTimeOut(time:Long):DefaultOkHttpClient{
        this.mConnectionTimeOut = time
        return this
    }
    fun setWriteTimeOut(time:Long):DefaultOkHttpClient{
        this.mWriteTimeOut = time
        return this
    }

    fun setReaderTimeOut(time:Long):DefaultOkHttpClient{
        this.mReadTimeOut = time
        return this
    }
    fun isRetryOnConnectionFailure(isRetry:Boolean):DefaultOkHttpClient{
        this.isRetryOnConnectionFailure = isRetry
        return this
    }
    fun setCookieJar(cookieJar:CookieJar):DefaultOkHttpClient{
        this.mCookieJar = cookieJar
        return this
    }
    fun setInterceptors(interceptors:Array<Interceptor>):DefaultOkHttpClient{
        this.mInterceptors = interceptors
        return this
    }
}

上面的getCookieJar方法可以实现在发送请求时候,CookieJar方法会回调loadForRequestcookie加入request header里面,在请求响应的时候,Cookjar会回调saveFromResponse方法,从而读取response header里面的cookie。这是只是简单的配置下,这里暂时没用到cookie的使用,在注入的时候还可以进一步cookie持久化和保存在本地,比如实现用户的自动登录功能。

上面还提供了请求时间的配置和拦截器的配置,这样就可以在Application注入的时候进一步配置

GsonConverterFactory

创建Retrofit的时候,我们还需要传入GsonConverterFactory,做为请求返回json使用Gson来使用,其中GsonConverterFactory可以传入一个Gson对象,统一做一些数据的序列化和反序列化数据处理。其中TypeAdapter是同时对数据进行序列化处理和反序列化处理,或者单独的通过JsonSerializer进行序列化,以及JsonDeserializer反序列化,如下

class GsonConverter {
    fun <T> createGson():Gson{
        return GsonBuilder()
                .registerTypeAdapter(String::class.java, NullStringAdapter())
                .registerTypeAdapter(Long::class.java, LongDeserializer())
                .registerTypeAdapter(Double::class.java, DoubleDeserializer())
                .registerTypeAdapter(Date::class.java, DateSerializer())
                .registerTypeAdapter(Date::class.java, DateDeserializer())
                .registerTypeAdapter(ResponseWrapper::class.java,ResponseWrapperDeserializer<T>())
                .create()
    }
}

创建GsonBuilder然后通过registerTypeAdapter注入需要处理的一些 TypeAdapter或者JsonSerializerJsonDeserializer等等

private class NullStringAdapter : TypeAdapter<String>() {

    override fun read(reader: JsonReader): String {
        if (reader.peek() == JsonToken.NULL) {
            reader.nextNull()
            return ""
        }

        return reader.nextString()
    }

    override fun write(writer: JsonWriter, value: String?) {
        if (value == null) {
            writer.nullValue()
            return
        }

        writer.value(value)
    }
}

NullStringAdapterString类型的NULL转换为"",

private class DateSerializer : JsonSerializer<Date> {
    override fun serialize(src: Date?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement? {
        return if (src == null) null else JsonPrimitive(src.time / 1000)
    }
}

Date数据类型序列化的时间戳转换到精确到秒

private class DateDeserializer : JsonDeserializer<Date> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Date? {
        return if (json == null || json.asLong == 0L) null else Date(json.asLong * 1000)
    }
}

Date数据类型反序列化的时间戳转换到精确到毫秒

private class DoubleDeserializer : JsonDeserializer<Double> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Double {
        return if (json == null || TextUtils.isEmpty(json.asString)) 0.0 else json.asDouble
    }
}

Double数据类型反序列化JSON返回NULL或者为空的时候返回默认值

private class LongDeserializer : JsonDeserializer<Long> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Long {
        return if (json == null || TextUtils.isEmpty(json.asString)) 0 else json.asLong
    }
}

Long数据类型反序列化JSON返回NULL或者为空的时候返回默认值

private class ResponseWrapperDeserializer<T>:JsonDeserializer<ResponseWrapper<T>>{
    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): ResponseWrapper<T> {
        val jsonObj = json.asJsonObject
        val code = jsonObj.get("code").asInt
        val msg = jsonObj.get("msg").asString
        val version = jsonObj.get("version").asString
        val timestamp = jsonObj.get("timestamp").asLong
        val data = context.deserialize<T>(jsonObj.get("data"), (typeOfT as ParameterizedType).actualTypeArguments[0])
        return ResponseWrapper(code,msg,version,timestamp,data)
    }
}

这个主要是处理Java在编译的时候会擦除泛型,如果不处理,在Obserable的时候就无法传入泛型了。其中ResponseWrapper处理JSON的第一层统一样式,比如这里的

{
"code":0,
"msg":"",
"version":"",
"timestamp",12324334,
"data":T
}

其中上面的data可以是对象,也可以是数组,所以这里我们返回的时候就可以用泛型传入,ResponseWrapper如下

data class ResponseWrapper<out T>(val code:Int = -1,
                                  val msg:String = "",
                                  val version:String = "",
                                  val timestamp:Long = 0,
                                  @Transient val data: T): Serializable

Retrofit

OkHttpGson准备好后,接下来就可以创建Retrofit对象了。通过单例实现

object ApiService {
    private var mIClient:IClient = DefaultOkHttpClient()
    private val mRetrofitMap:MutableMap<String,Retrofit> = mutableMapOf()
    fun <T> get(baseUrl: String, service: Class<T>): T {
        return this.getRetrofit<T>(baseUrl).create(service)
    }

    private fun <T> getRetrofit(baseUrl: String):Retrofit{
        if(baseUrl.isEmpty()){
            throw IllegalArgumentException("baseUrl can not be empty")
        }
        if(mRetrofitMap[baseUrl] != null){
            return mRetrofitMap[baseUrl]!!
        }
        val mRetrofit = Retrofit.Builder()
                .baseUrl(baseUrl)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(GsonConverter().createGson<T>()))
                .client(mIClient.getClient()).build()
        mRetrofitMap.put(baseUrl,mRetrofit)
        return mRetrofit
    }
    fun registerClient(client:IClient){
        this.mIClient = client
    }

}

IClient是传入的OkHttp对象,默认实例化DefaultOkHttpClient;
mRetrofitMap保存传入的BaseUrl对应的Retrofit对象,实现一一对应的关系;
get方法供外界调用传入BaseUrl和接口Service,以及Response转换的data对应的bean;
getRetrofit方法,当通过在mRetrofitMap找不到Retrofit对象的时候,就创建Retrofit对象。同时保存;
registerClient供在Application里面注入自定义的OkHttp对象

注入

准备好后接下来就是在ApplicationonCreate方法里面注入OkHttp实现一些拦截器

ApiService
        .registerClient(DefaultOkHttpClient()
              .setInterceptors(arrayOf(
               OkHttpLogInterceptor().getInterceptor(),
               BasicParamsInterceptor().getInterceptor(),
               ResponseInterceptor().getInterceptor())))

其中OkHttpLogInterceptor是通过上面说的 okhttp3.logging库创建的,主要是拦截http请求日志

class OkHttpLogInterceptor:IInterceptor {
    override fun getInterceptor(): Interceptor {
        val mHttpLogInter = HttpLoggingInterceptor{
            message ->
            Timber.d("HttpLogging=====$message")
        }
        mHttpLogInter.level = HttpLoggingInterceptor.Level.BODY
        return mHttpLogInter
    }
}

BasicParamsInterceptor是传入一些公共的参数,可以自己在注入的时候传入,也可使用默认的一套公共参数

class BasicParamsInterceptor(val mQueryParameters : MutableMap<String,String>? = null):IInterceptor {
    override fun getInterceptor(): Interceptor {
        return Interceptor { chain ->
            val originalRequest = chain.request()
            val originalHttpUrl = originalRequest.url()
            val newUrl = originalHttpUrl
                    .newBuilder()
                    .addQueryParameters(mQueryParameters?:defaultBaseParameters(originalHttpUrl))
                    .build()
            val newRequest = originalRequest
                    .newBuilder()
                    .url(newUrl)
                    .method(originalRequest.method(),originalRequest.body())
                    .build()
            chain.proceed(newRequest)
        }
    }
    fun HttpUrl.Builder.addQueryParameters(mQueryParameters : MutableMap<String,String>):HttpUrl.Builder{
        if(mQueryParameters.isNotEmpty()){
            mQueryParameters.forEach {
                this.addQueryParameter(it.key,it.value)
            }
        }
        return this
    }
    fun defaultBaseParameters(originalHttpUrl:HttpUrl):MutableMap<String,String>{
        return mutableMapOf("version" to Consts.API_VERSION,
                            "platform" to Consts.API_PLATFORM,
                            "methodName" to originalHttpUrl.encodedPath().split("/").last(),
                            "token" to "")
    }
}

ResponseInterceptor是一些异常code处理,

class ResponseInterceptor(val handlerResponseException:((response:Response)->Unit)?=null):IInterceptor {
    override fun getInterceptor(): Interceptor {
        return Interceptor {
            chain ->
            val response = chain.proceed(chain.request())
            handlerResponseException?.invoke(response)
            when(response.code()){
                200 -> response
                10001 -> throw TokenExpiredException(response.code(), response.message())
                else -> throw RequestException(response.code(), response.message())
            }
        }
    }
}

其他的code处理都可以在这里处理,或者结合RxBus进行进一步的操作。

接口

定义一个接口

interface CommService {
    @FormUrlEncoded
    @POST("ArticleList")
    fun articleList(@Field("page") page:Int = 0,@Field("size") size:Int = 15,@Field("id") id:Long): Observable<ResponseWrapper<ArticleListResponse>>
}

注意,返回的bean里面不能用泛型,否则会报错,这里ArticleListResponsejson返回的数据,这里只是随便定义一些数据。

open class BaseResponse:Serializable

open class PageResponse : BaseResponse() {
    var page = 0
    var size = 0
    var total = 0
}

class ArticleListResponse : PageResponse() {
    val data: List<Item> = emptyList()

    class Item(
            val id: Long,
            val title: String,
            val image: String,
            val desp: String,
    ) : Serializable
}

接下来提供一个AppModel提交请求

object AppModel {
    fun articleList(pageInfo: PageInfo,id:Long = 0):Observable<ArticleListResponse>{
        return ApiClient(CommService::class.java).articleList(pageInfo.page,pageInfo.size,id).responseWrapperLogic()
    }
    fun <T> ApiClient(service: Class<T>):T{
        return ApiService.get(BuildConfig.BASE_URL,service)
    }
    private fun <T> Observable<ResponseWrapper<T>>.responseWrapperLogic() =
            map { it.data}.compose{it.subscribeOn(Schedulers.io())}.observeOn(AndroidSchedulers.mainThread())
}

这里只是随便定义接口方法而已,可以根据自己相应的接口添加。

使用

最简单调用

AppModel.articleList(page,1).subscribeWith ({},{}).bindTo(mvpView.sub)

接下来可以进一步结合compose来写请求的加载框或者其他加载动画。其中subscribeWith是定义的一个DisposableObserver<T>bindTo是结合 CompositeDisposable一起绑定多个Disposable,后面可以更好的管理绑定和解绑

fun <T> Observable<T>.subscribeWith(onNext:((res: T) ->Unit)?=null,
                                    onError:((e: Throwable) ->Unit)?=null,
                                    onComplete:(() ->Unit)?=null):DisposableObserver<T>{
    return this.subscribeWith(object : DisposableObserver<T>() {
        override fun onError(e: Throwable) {
            if(onError!=null) onError(e)
        }
        override fun onComplete() {
            if(onComplete!=null) onComplete()
        }

        override fun onNext(res: T) {
            if(onNext!=null) onNext(res)
        }
    })
}
fun  <T> DisposableObserver<T>.bindTo(sub: CompositeDisposable) {
    sub.add(this)
}

mvpView.sub是在BasePresenter里面定义的CompositeDisposable对象,在下一篇Android搭建应用框架系列之MVP封装进一步讲解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容