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()
                    }
                })
    }
}

效果如下:


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章