Kotlin - Retrofit2和Rxjava2封装的网络请求类(含图片上传)

文/ZYRzyr
原文链接:http://www.jianshu.com/p/c66d50cd14ee

阅读建议:本文适合熟悉RetrofitRxjava2的同学阅读,其中也包含一丢丢的RxLifecycle,文中不包含这两个库的使用说明。不熟悉RxJava的同学,建议去这里了解,里面包含3篇文章,均通俗易懂。

提示:文中使用的RxJava2的类均是不支持背压的,即Observable(被观察者)Observer(观察者)。需要背压策略,请自行替换为对应的Flowable(被观察者)Subscriber(观察者)即可。如果想使用Kotlin的一些便利的语法,可以将RxJava依赖换成RxKotlin即可。

开始

1.按惯例先添加依赖:

//Retrofit相关
compile(['com.squareup.okhttp3:logging-interceptor:3.9.0',//用于查看http请求时的log
         'com.squareup.retrofit2:retrofit:2.3.0',
         'com.squareup.retrofit2:adapter-rxjava2:2.3.0',
         'com.squareup.retrofit2:converter-gson:2.3.0'])

//RxJava相关
compile(['io.reactivex.rxjava2:rxandroid:2.0.1',
         'io.reactivex.rxjava2:rxjava:2.1.3'])       //此处可换成'io.reactivex.rxjava2:rxkotlin:2.1.0'

//RxLifecycle相关
compile(['com.trello.rxlifecycle2:rxlifecycle-kotlin:2.2.0',
         'com.trello.rxlifecycle2:rxlifecycle-components:2.2.0'])

提示:可以去RetrofitRxjava2(RxAndroid)okhttpRxLifecycle,查询最新版本号。

2.封装请求类

为了秉承RxJava的链式调用风格,也为了方便每一个API的调用操作,创建了一个单例类ApiClient,具体如下:

class ApiClient private constructor() {
    lateinit var service: GitHubService

    private object Holder {
        val INSTANCE = ApiClient()
    }

    companion object {
        val instance by lazy { Holder.INSTANCE }
    }

    fun init() {  //在Application的onCreate中调用一次即可
        val okHttpClient = OkHttpClient().newBuilder()
                 //输入http连接时的log,也可添加更多的Interceptor
                .addInterceptor(HttpLoggingInterceptor().setLevel( 
                        if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
                        else HttpLoggingInterceptor.Level.NONE
                ))
                .build()

        val retrofit = Retrofit.Builder()
                .baseUrl("https://api.github.com/")   //本文以GitHub API为例
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .client(okHttpClient)
                .build()

        service = retrofit.create(GitHubService::class.java)
    }
}

其中使用GitHub的的API作为测试,GitHubService如下:

interface GitHubService {
//请添加相应的`API`调用方法
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Observable<List<Repo>> //每个方法的返回值即一个Observable
}

上面的Repo即一个简单的Kotlin数据类,由于字较多,就不贴出来了,具体可去文末Demo地址查找。

3.RESTful API请求响应的处理

API的响应返回形式有很多种,此处介绍最常见的两种形式的处理:标准RESTful API任性的后端写的APIGitHub提供的API即标准RESTful API
RESTful API的请求响应主要处理状态码与数据体,具体封装如下:

abstract class ApiResponse<T>(private val context: Context) : Observer<T> {
    abstract fun success(data: T)
    abstract fun failure(statusCode: Int, apiErrorModel: ApiErrorModel)

    override fun onSubscribe(d: Disposable) {
        LoadingDialog.show(context)
    }

    override fun onNext(t: T) {
        success(t)
    }

    override fun onComplete() {
        LoadingDialog.cancel()
    }

    override fun onError(e: Throwable) {
        LoadingDialog.cancel()
        if (e is HttpException) { //连接服务器成功但服务器返回错误状态码
            val apiErrorModel: ApiErrorModel = when (e.code()) {
                ApiErrorType.INTERNAL_SERVER_ERROR.code ->
                    ApiErrorType.INTERNAL_SERVER_ERROR.getApiErrorModel(context)
                ApiErrorType.BAD_GATEWAY.code ->
                    ApiErrorType.BAD_GATEWAY.getApiErrorModel(context)
                ApiErrorType.NOT_FOUND.code ->
                    ApiErrorType.NOT_FOUND.getApiErrorModel(context)
                else -> otherError(e)

            }
            failure(e.code(), apiErrorModel)
            return
        }

        val apiErrorType: ApiErrorType = when (e) {  //发送网络问题或其它未知问题,请根据实际情况进行修改
            is UnknownHostException -> ApiErrorType.NETWORK_NOT_CONNECT
            is ConnectException -> ApiErrorType.NETWORK_NOT_CONNECT
            is SocketTimeoutException -> ApiErrorType.CONNECTION_TIMEOUT
            else -> ApiErrorType.UNEXPECTED_ERROR
        }
        failure(apiErrorType.code, apiErrorType.getApiErrorModel(context))
    }

    private fun otherError(e: HttpException) =
            Gson().fromJson(e.response().errorBody()?.charStream(), ApiErrorModel::class.java)
}

说明:
1.每个响应继承Observer,其中的泛型以适配返回的不同的数据体;
2.定义两个抽象方法successfailure,在使用的时候只需关注成功和失败这两种情况;
3.在onSubscribe即开始请求的时候显示Loading,在请求完成或出错时隐藏;
4.在onNextObserver成功接收数据后直接调用success,在调用处可直接使用返回的数据;
5.在onError即请求出错时处理,此处包含两种情况:连接服务器成功但服务器返回错误状态码、网络或其它问题。

在错误处理中,定义了一个枚举类ApiErrorType,用于列举出服务器定义的错误状态码情况:

enum class ApiErrorType(val code: Int, @param: StringRes private val messageId: Int) {
//根据实际情况进行增删
    INTERNAL_SERVER_ERROR(500, R.string.service_error), 
    BAD_GATEWAY(502, R.string.service_error),
    NOT_FOUND(404, R.string.not_found),
    CONNECTION_TIMEOUT(408, R.string.timeout),
    NETWORK_NOT_CONNECT(499, R.string.network_wrong),
    UNEXPECTED_ERROR(700, R.string.unexpected_error);

    private val DEFAULT_CODE = 1

    fun getApiErrorModel(context: Context): ApiErrorModel {
        return ApiErrorModel(DEFAULT_CODE, context.getString(messageId))
    }
}

还定义了一个错误消息的的实体类ApiErrorModel(在Kotlin中即为一个数据类),用于包含错误信息提示用户或服务器返回的错误信息以提示开发人员:

data class ApiErrorModel(var status: Int, var message: String)

4.线程与生命周期

RxJava的一大特色即方便的线程切换操作,在请求API中需要进行线程的切换,通常是以下形式(伪代码):

observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())

但每个请求都写一段这个,显得特别麻烦,所以进行以下简单封装:

object NetworkScheduler {
    fun <T> compose(): ObservableTransformer<T, T> {
        return ObservableTransformer { observable ->
            observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
        }
    }
}

使用的时候简单搞定,伪代码如下:

observable.compose(NetworkScheduler.compose())

Android中,当一个Activity在调APIonDestroy了,需要取消请求,所以此处引入了RxLifecycle进行管理:
Activity继承RxAppCompatActivity后,在observable的调用链中加入.bindUntilEvent(this, ActivityEvent.DESTROY)即可,伪代码如下:

observable.compose(NetworkScheduler.compose())
          .bindUntilEvent(this, ActivityEvent.DESTROY)  //加入这句
          .subscribe(...)

5.使用

以上准备工作完成后,即可开始使用:
首先在Application中初始化ApiClient

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        ApiClient.instance.init() //这里
    }
}

在需要的地方使用ApiClient,如本文Demo,点击按钮时,请求数据,成功后用TextView显示出来:

class MainActivity : RxAppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        submit.setOnClickListener { fetchRepo() } //按钮点击事件
    }

    private fun fetchRepo() {
        //链式调用
        ApiClient.instance.service.listRepos(inputUser.text.toString())   //GitHubService中的方法
                .compose(NetworkScheduler.compose())                      //线程切换处理
                .bindUntilEvent(this, ActivityEvent.DESTROY)              //生命周期管理
                .subscribe(object : ApiResponse<List<Repo>>(this) {       //对象表达式约等于Java中的匿名内部类 
                    override fun success(data: List<Repo>) {              //请求成功,此处显示一些返回的数据
                        userName.text = data[0].owner.login
                        repoName.text = data[0].name
                        description.text = data[0].description
                        url.text = data[0].html_url
                    }

                    override fun failure(statusCode: Int, apiErrorModel: ApiErrorModel) { //请求失败,此处直接显示Toast
                        Toast.makeText(this@MainActivity, apiErrorModel.message, Toast.LENGTH_SHORT).show()
                    }
                })
    }
}

效果如下:


效果.gif

6.任性的后端写的API请求响应的处理

这种情况只需要对数据类和响应处理进行修改即可。有些后端开发者们,可能将返回体写成如下形式:

{
    "code": "200",
    "data": [
        {
            "name": "Tom",
            "age": 12,
            "money": 100.5
        },
        {
            "name": "Bob",
            "age": 13,
            "money": 200.5
        }
    ],
    "message": "客户端请求成功"
}

所有返回的数据中,最外层都包裹了一层信息,以表示请求成功或失败,中间data才是具体数据,所以定义数据类(实体类)时,需要定义成如下形式:

data class ResponseWrapper<T>(var code: Int, var data: T, var message: String)

其中data为泛型,以适配不同的数据体。
然后将上文第3点中的ApiResponse修改如下:

abstract class RequestCallback<T>(private val context: Context) : Observer<ResponseWrapper<T>> {
    abstract fun success(data: T)
    abstract fun failure(statusCode: Int, apiErrorModel: ApiErrorModel)

    private object Status {
        val SUCCESS = 200
    }

    override fun onSubscribe(d: Disposable) {
        LoadingDialog.show(context)
    }

    override fun onNext(t: ResponseWrapper<T>) {
        if (t.code == Status.SUCCESS) {
            success(t.data)
            return
        }

        val apiErrorModel: ApiErrorModel = when (t.code) {
            ApiErrorType.INTERNAL_SERVER_ERROR.code ->
                ApiErrorType.INTERNAL_SERVER_ERROR.getApiErrorModel(context)
            ApiErrorType.BAD_GATEWAY.code ->
                ApiErrorType.BAD_GATEWAY.getApiErrorModel(context)
            ApiErrorType.NOT_FOUND.code ->
                ApiErrorType.NOT_FOUND.getApiErrorModel(context)
            else -> ApiErrorModel(t.code, t.message)
        }
        failure(t.code, apiErrorModel)
    }

    override fun onComplete() {
        LoadingDialog.cancel()
    }

    override fun onError(e: Throwable) {
        LoadingDialog.cancel()
        val apiErrorType: ApiErrorType = when (e) {
            is UnknownHostException -> ApiErrorType.NETWORK_NOT_CONNECT
            is ConnectException -> ApiErrorType.NETWORK_NOT_CONNECT
            is SocketTimeoutException -> ApiErrorType.CONNECTION_TIMEOUT
            else -> ApiErrorType.UNEXPECTED_ERROR
        }
        failure(apiErrorType.code, apiErrorType.getApiErrorModel(context))
    }
}

使用方式:
1.先在GitHubService.kt中新增如下方法:

@GET("xxx/xxx")
fun repos(@Path("user") user: String): Observable<ResponseWrapper<List<Repo>>>

2.之后与上文第5点相同。

2017年10月13日更新—增加上传图片的方法
新增OkHttpUtil.kt,用于上传图片,代码如下:

object OkHttpUtil {
    fun createTextRequestBody(source: String): RequestBody
            = RequestBody.create(MediaType.parse("text/plain"), source)

    fun createPartWithAllImageFormats(requestKey: String, file: File): MultipartBody.Part
            = MultipartBody.Part
            .createFormData(requestKey, file.name, RequestBody.create(MediaType.parse("image/*"), file))
}

使用方式:
1.先在GitHubService.kt中新增如下方法:

@Multipart
@POST("xxxx/xxxx") //This is imaginary URL
fun updateImage(@Part("name") name: RequestBody,
                @Part image: MultipartBody.Part): Observable<UserInfo>

2.在需要的地方使用:

ApiClient.instance.service.updateImage(OkHttpUtil.createTextRequestBody("Bob"),
                 OkHttpUtil.createPartWithAllImageFormats("avatar",file))   //此处调用OkHttpUtil中的方法
                .compose(NetworkScheduler.compose())
                .bindUntilEvent(this,ActivityEvent.DESTROY)
                .subscribe(object : ApiResponse<UserInfo>(this) {
                    override fun success(data: UserInfo) {
                        //Do something
                    }

                    override fun failure(statusCode: Int, apiErrorModel: ApiErrorModel) {
                        //Do something
                    }
                })



最后
希望本文对您有所帮助。如果文中有什么表述不当的地方,请在下方评论,以帮助我改正。

Demo地址:https://github.com/ZYRzyr/ApiClient (欢迎Star和Follow)

原文作者/ZYRzyr
原文链接:http://www.jianshu.com/p/c66d50cd14ee

请进入这里获取授权:https://101709080007647.bqy.mobi

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 成功者之所以说坚持等于胜利,这是因为他已经是过来人,他们是有发言权的。但对大多数普通人来说,前途既是未知,可能咬着...
    未文啊阅读 119评论 0 0
  • 亲子日记第四十二天,今天是农历五月二十八,是儿子的生日,上午去蛋糕店做了一个蛋糕送到儿子幼儿园,老师小朋友...
    AA稳稳阅读 276评论 0 4
  • 在和学生讨论民国四大才女的时候,学生问我为什么张爱玲留下的作品最多、最出名。我戏言因为张爱玲活得年纪最大啊...
    邰枫的台阅读 526评论 0 2