精华内容
下载资源
问答
  • 2022-05-11 11:45:03

    RxHttp

    项目地址:kongpf8848/RxHttp 

    简介: 基于 RxJava2+Retrofit+OkHttp4.x 封装的网络请求类库,亮点多多,完美兼容 MVVM(ViewModel,LiveData),天生支持网络请求和生命周期绑定,天生支持多 BaseUrl,支持文件上传下载进度监听,支持断点下载,支持 Glide 和网络请求公用一个 OkHttpClient

    更多:作者   提 Bug   

    标签:

    基于 RxJava2+Retrofit 2.9.0+OkHttp 4.9.0 实现的轻量级,完美兼容 MVVM 架构的网络请求封装类库,小巧精致,简单易用,网络请求原来如此简单:smirk::smirk::smirk:

    亮点

    • 代码量极少,类库体积不足 100kb,但足以胜任大部分 APP 的网络请求任务,浓缩的都是精华啊^

    • 完美兼容 MVVM,MVC 架构,兼容 Kotlin 和 Java,Kotlin+MVVM+RxHttp 结合起来使用更酸爽,MVVM 官方推荐,抱紧 Google 大腿就对了

    • 完美解决泛型类型擦除的棘手问题,还原泛型的真实类型

    • 天生支持网络请求和 Activity,Fragment 生命周期绑定,界面销毁时自动取消网络请求回调

    • 天生支持多 BaseUrl,支持动态传入 Url

    • 支持自定义 OkHttpClient.Builder,可高度自定义网络请求参数,支持 Https 证书单向校验(客户端校验服务端证书)

    • 支持 Glide 等和 Http 请求公用一个 OkHttpClient,充分利用 OkHttpClient 的线程池和连接池,大部分情况下一个 App 一个 OkHttpClient 就够了

    • 支持 GET,POST,PUT,DELETE 等请求方式,支持文件上传及进度监听,支持同时上传多个文件,支持 Uri 上传

    • 支持文件下载及进度监听,支持大文件下载,支持断点下载

    使用要求

    项目基于 AndroidX,Java8+,minSdkVersion>=21

    使用

    • 在项目根目录的 build.gradle 文件中添加:
      allprojects {
        repositories {
            mavenCentral()
        }
      }
      
    • 在具体 Module 的 build.gradle 文件中添加:
      implementation 'io.github.kongpf8848:RxHttp:1.0.12'
      

    配置(可选)

      RxHttpConfig.getInstance()
        /**
         * 失败重试次数
         */
        .maxRetries(3)
        /**
         * 每次失败重试间隔时间
         */
        .retryDelayMillis(200)
        /**
         * Https 证书校验,单向校验,即客户端校验服务端证书,null 则为不校验
         */
         //.certificate(AssetUtils.openFile(applicationContext,"xxx.cer"))
        /**
         * 设置 OkHttpClient.Builder(),RxHttp 支持自定义 OkHttpClient.Builder()
         */
        .getBuilder().apply {
            connectTimeout(60, TimeUnit.SECONDS)
            readTimeout(60, TimeUnit.SECONDS)
            writeTimeout(60, TimeUnit.SECONDS)
            /**
             * DEBUG 模式下,添加日志拦截器,建议使用 RxHttp 中的 FixHttpLoggingInterceptor,使用 OkHttp 的 HttpLoggingInterceptor 在上传下载的时候会有 IOException 问题
             */
            if (BuildConfig.DEBUG) {
                addInterceptor(FixHttpLoggingInterceptor().apply {
                    level = FixHttpLoggingInterceptor.Level.BODY
                })
            }
        }
    

    基础使用

    • GET/POST/PUT/DELETE/上传请求
       RxHttp.getInstance()
        /**
         * get:请求类型,可为 get,post,put,delete,upload,分别对应 GET/POST/PUT/DELETE/上传请求
         * context:上下文,可为 Context,Activity 或 Fragment 类型,当 context 为 Activity 或 Fragment 时网络请求和生命周期绑定
         */
        .get(context)
        /**
         * 请求 url,如 https://www.baidu.com
         */
        .url("xxx")
        /**
         *请求参数键值对,类型为 Map<String, Any?>?,如 hashMapOf("name" to "jack")
         */
        .params(map)
        /**
         *每个网络请求对应的 tag 值,可为 null,用于后续手动根据 tag 取消指定网络请求
         */
        .tag("xxx")
        /**
         * HttpCallback:网络回调,参数 xxx 为返回数据对应的数据模型,
         * 类似 RxJava 中的 Observer,onComplete 只有在 onNext 回调之后执行,如发生错误则只会回调 onError 而不会执行 onComplete
         */
        .enqueue(object : HttpCallback<xxx>() {
            /**
             * http 请求开始时回调
             */
            override fun onStart() {
    
            }
    
            /**
             * http 请求成功时回调
             */
            override fun onNext(response: xxx?) {
    
            }
    
            /**
             * http 请求失败时回调
             */
            override fun onError(e: Throwable?) {
    
            }
    
            /**
             * http 请求成功完成时回调
             */
            override fun onComplete() {
    
            }
    
            /**
             * 上传进度回调,请求类型为 upload 时才会回调
             */
            override fun onProgress(readBytes: Long, totalBytes: Long) {
    
            }
        })
    
    • 下载请求
       RxHttp.getInstance()
          /**
           * download:请求类型,下载请求
           * context:上下文,如不需要和生命周期绑定,应该传递 applicationContext
           */
          .download(context)
          /**
           * 保存路径
           */
          .dir(dir)
          /**
           *保存文件名称
           */
          .filename(filename)
          /**
           * 是否为断点下载,默认为 false
           */
          .breakpoint(true)
          /**
           * 下载地址,如 http://study.163.com/pub/ucmooc/ucmooc-android-official.apk
           */
          .url(url)
          /**
           * 请求 Tag
           */
          .tag(null)
          /**
           * 下载回调
           */
          .enqueue(object: DownloadCallback() {
              /**
               * 下载开始时回调
               */
              override fun onStart() {
    
              }
    
              /**
               * 下载完成时回调
               */
              override fun onNext(response: DownloadInfo?) {
    
              }
    
              /**
               * 下载失败时回调
               */
              override fun onError(e: Throwable?) {
    
              }
    
              /**
               * 下载完成之后回调
               */
              override fun onComplete() {
    
              }
    
              /**
               * 下载进度回调
               */
              override fun onProgress(readBytes: Long, totalBytes: Long) {
    
              }
    
          })
    
    • 取消请求
        /**
         * tag:Any?,请求 Tag,对应网络请求里的 Tag 值
         * 如不为 null,则取消指定网络请求,
         * 如为 null,则取消所有网络请求
         */
        RxHttp.getInstance().cancelRequest(tag)
    

    项目实战

    此处假设服务端返回的数据格式为{"code":xxx,"data":T,"msg":""},其中 code 为响应码,整型,等于 200 时为成功,其余为失败,data 对应的数据类型为泛型(boolean,int,double,String,对象{ },数组[ ]等类型)

     {
        "code": 200,
        "data":T,
        "msg": ""
    }
    

    对应的 Response 类为

    class TKResponse<T>(val code:Int,val msg: String?, val data: T?) : Serializable {
        companion object{
            const val STATUS_OK=200
        }
        fun isSuccess():Boolean{
            return code== STATUS_OK
        }
    }
    
    • MVC 项目

      • 定义 MVCHttpCallback,用于将网络请求结果回调给 UI 界面
    ```kotlin
    

    abstract class MVCHttpCallback {

        private val type: Type
    
        init {
            val arg = TypeUtil.getType(javaClass)
            type = TypeBuilder
                    .newInstance(TKResponse::class.java)
                    .addTypeParam(arg)
                    .build()
        }
    
        fun getType(): Type {
            return this.type
        }
    
        /**
         * 请求开始时回调,可以在此加载 loading 对话框等,默认为空实现
         */
        open fun onStart() {}
    
        /**
         * 抽象方法,请求成功回调,返回内容为泛型,对应 TKResponse 的 data
         */
        abstract fun onSuccess(result: T?)
    
        /**
         * 抽象方法,请求失败回调,返回内容为 code(错误码),msg(错误信息)
         */
        abstract fun onFailure(code: Int, msg: String?)
    
        /**
         * 上传进度回调,默认为空实现
         */
        open fun onProgress(readBytes: Long, totalBytes: Long) {}
    
        /**
         * 请求完成时回调,请求成功之后才会回调此方法,默认为空实现
         */
        open fun onComplete() {}
    

    }

    
       * 定义网络接口,封装 GET/POST 等网络请求
    
    
       ```kotlin
       object MVCApi {
    
           /**
            * GET 请求
            * context:上下文
            * url:请求 url
            * params:参数列表,可为 null
            * tag:标识一个网络请求
            * callback:网络请求回调
            */
           inline fun <reified T> httpGet(
                   context: Context,
                   url: String,
                   params: Map<String, Any?>?,
                   tag: Any? = null,
                   callback: MVCHttpCallback<T>
           ) {
               RxHttp.getInstance().get(context)
                       .url(url)
                       .params(params)
                       .tag(tag)
                       .enqueue(simpleHttpCallback(callback))
           }
    
           /**
            * POST 请求
            * context:上下文
            * url:请求 url
            * params:参数列表,可为 null
            * tag:标识一个网络请求
            * callback:网络请求回调
            */
           inline fun <reified T> httpPost(
                   context: Context,
                   url: String,
                   params: Map<String, Any?>?,
                   tag: Any? = null,
                   callback: MVCHttpCallback<T>
           ) {
               RxHttp.getInstance().post(context)
                       .url(url)
                       .params(params)
                       .tag(tag)
                       .enqueue(simpleHttpCallback(callback))
           }
    
           ......
    
            inline fun <reified T> simpleHttpCallback(callback: MVCHttpCallback<T>): HttpCallback<TKResponse<T>> {
               return object : HttpCallback<TKResponse<T>>(callback.getType()) {
                   override fun onStart() {
                       super.onStart()
                       callback.onStart()
                   }
    
                   override fun onNext(response: TKResponse<T>?) {
                       if (response != null) {
                           if (response.isSuccess()) {
                               callback.onSuccess(response.data)
                           } else {
                               return onError(ServerException(response.code, response.msg))
                           }
    
                       } else {
                           return onError(NullResponseException(TKErrorCode.ERRCODE_RESPONSE_NULL, TKErrorCode.ERRCODE_RESPONSE_NULL_DESC))
                       }
    
                   }
    
                   override fun onError(e: Throwable?) {
                       handleThrowable(e).run {
                           callback.onFailure(first, second)
                       }
                   }
    
                   override fun onComplete() {
                       super.onComplete()
                       callback.onComplete()
                   }
    
                   override fun onProgress(readBytes: Long, totalBytes: Long) {
                       super.onProgress(readBytes, totalBytes)
                       callback.onProgress(readBytes, totalBytes)
                   }
               }
           }
    
     * 在 View 层如 Activity 中调用网络接口
    
    
     ```kotlin
     MVCApi.httpGet(
         context = baseActivity,
         url = TKURL.URL_GET,
         params = null,
         tag = null, 
         callback = object : MVCHttpCallback<List<Banner>>() {
         override fun onStart() {
             LogUtils.d(TAG, "onButtonGet onStart() called")
         }
    
         override fun onSuccess(result: List<Banner>?) {
             Log.d(TAG, "onButtonGet onSuccess() called with: result = $result")
         }
    
         override fun onFailure(code: Int, msg: String?) {
             Log.d(TAG, "onButtonGet onFailure() called with: code = $code, msg = $msg")
         }
    
         override fun onComplete() {
             Log.d(TAG, "onButtonGet onComplete() called")
         }
    
     })
     ```
    
     **具体使用可以参考 demo 代码,demo 中有详细的示例演示 MVC 项目如何使用 RxHttp**
    
    • MVVM 项目

      • 定义 Activity 基类 BaseMvvmActivity

        abstract class BaseMvvmActivity<VM : BaseViewModel, VDB : ViewDataBinding> : AppCompatActivity(){
        
         lateinit var viewModel: VM
         lateinit var binding: VDB
        
         protected abstract fun getLayoutId(): Int
        
         final override fun onCreate(savedInstanceState: Bundle?) {
             onCreateStart(savedInstanceState)
             super.onCreate(savedInstanceState)
             binding = DataBindingUtil.setContentView(this, getLayoutId())
             binding.lifecycleOwner = this
             createViewModel()
             onCreateEnd(savedInstanceState)
         }
        
         protected open fun onCreateStart(savedInstanceState: Bundle?) {}
         protected open fun onCreateEnd(savedInstanceState: Bundle?) {}
        
         /**
          * 创建 ViewModel
          */
         private fun createViewModel() {
             val type = findType(javaClass.genericSuperclass)
             val modelClass = if (type is ParameterizedType) {
                 type.actualTypeArguments[0] as Class<VM>
             } else {
                 BaseViewModel::class.java as Class<VM>
             }
             viewModel = ViewModelProvider(this).get(modelClass)
         }
        
         private fun findType(type: Type): Type?{
             return when(type){
                 is ParameterizedType -> type
                 is Class<*> ->{
                 findType(type.genericSuperclass)
                 }
                 else ->{
                 null
                 }
             }
         }
        
        }
        
      • 定义 ViewModel 的基类 BaseViewModel

        open class BaseViewModel(application: Application) : AndroidViewModel(application) {
        
         /**
          * 网络仓库
          */
         protected val networkbaseRepository: NetworkRepository = NetworkRepository.instance
        
         /**
          * 上下文
          */
         protected var context: Context = application.applicationContext
        
        }
        
      • 定义网络仓库,封装网络接口 ```kotlin /**
      • MVVM 架构网络仓库
      • UI->ViewModel->Repository->LiveData(ViewModel)->UI */ class NetworkRepository private constructor() {

        companion object {

         val instance = NetworkRepository.holder
        

        }

        private object NetworkRepository {

         val holder = NetworkRepository()
        

        }

        inline fun wrapHttpCallback(): MvvmHttpCallback {

         return object : MvvmHttpCallback<T>() {
        
         }
        

        }

        inline fun newCallback(liveData: MutableLiveData>): HttpCallback> {

         val type = wrapHttpCallback<T>().getType()
         return object : HttpCallback<TKResponse<T>>(type) {
             override fun onStart() {
             liveData.value = TKState.start()
             }
        
             override fun onNext(response: TKResponse<T>?) {
             liveData.value = TKState.response(response)
             }
        
             override fun onError(e: Throwable?) {
             liveData.value = TKState.error(e)
             }
        
             override fun onComplete() {
        
             /**
              * 亲,此处不要做任何操作,不要给 LiveData 赋值,防止 onNext 对应的 LiveData 数据被覆盖,
              * 在 TKState 类 handle 方法里会特别处理回调的,放心好了
              */
             }
        
             override fun onProgress(readBytes: Long, totalBytes: Long) {
             liveData.value = TKState.progress(readBytes, totalBytes)
             }
         }
        

        }

        inline fun httpGet(

         context: Context,
         url: String,
         params: Map<String, Any?>?,
         tag: Any? = null
        

        ): MutableLiveData> {

         val liveData = MutableLiveData<TKState<T>>()
         RxHttp.getInstance()
             .get(context)
             .url(url)
             .params(params)
             .tag(tag)
             .enqueue(newCallback(liveData))
         return liveData
        

        }

        inline fun <reified T> httpPost(
            context: Context,
            url: String,
            params: Map<String, Any?>?,
            tag: Any? = null
        ): MutableLiveData<TKState<T>> {
            val liveData = MutableLiveData<TKState<T>>()
            RxHttp.getInstance().post(context)
                .url(url)
                .params(params)
                .tag(tag)
                .enqueue(newCallback(liveData))
            return liveData
        }
    
        inline fun <reified T> httpPostForm(
            context: Context,
            url: String,
            params: Map<String, Any?>?,
            tag: Any? = null
        ): MutableLiveData<TKState<T>> {
            val liveData = MutableLiveData<TKState<T>>()
            RxHttp.getInstance().postForm(context)
                .url(url)
                .params(params)
                .tag(tag)
                .enqueue(newCallback(liveData))
            return liveData
        }
    
        inline fun <reified T> httpPut(
            context: Context,
            url: String,
            params: Map<String, Any?>?,
            tag: Any? = null
        ): MutableLiveData<TKState<T>> {
            val liveData = MutableLiveData<TKState<T>>()
            RxHttp.getInstance().put(context)
                .url(url)
                .params(params)
                .tag(tag)
                .enqueue(newCallback(liveData))
            return liveData
        }
    
        inline fun <reified T> httpDelete(
            context: Context,
            url: String,
            params: Map<String, Any?>?,
            tag: Any? = null
        ): MutableLiveData<TKState<T>> {
            val liveData = MutableLiveData<TKState<T>>()
            RxHttp.getInstance().delete(context)
                .params(params)
                .url(url)
                .tag(tag)
                .enqueue(newCallback(liveData))
            return liveData
        }
    
    
        /**
         *上传
         *支持上传多个文件,map 中对应的 value 类型为 File 类型或 Uri 类型
         *支持监听上传进度
            val map =Map<String,Any>()
            map.put("model", "xiaomi")
            map.put("os", "android")
            map.put("avatar",File("xxx"))
            map.put("video",uri)
         */
        inline fun <reified T> httpUpload(
            context: Context,
            url: String,
            params: Map<String, Any?>?,
            tag: Any? = null
        ): MutableLiveData<TKState<T>> {
            val liveData = MutableLiveData<TKState<T>>()
            RxHttp.getInstance().upload(context)
                .url(url)
                .params(params)
                .tag(tag)
                .enqueue(newCallback(liveData))
            return liveData
        }
    
    
        /**
         * 下载
         * context:上下文,如不需要和生命周期绑定,应该传递 applicationContext
         * url:下载地址
         * dir:本地目录路径
         * filename:保存文件名称
         * callback:下载进度回调
         * md5:下载文件的 MD5 值
         * breakpoint:是否支持断点下载,默认为 true
         */
        fun httpDownload(context: Context, url: String, dir: String, filename: String, callback: DownloadCallback, md5: String? = null, breakPoint: Boolean = true, tag: Any? = null) {
            RxHttp.getInstance().download(context).dir(dir).filename(filename).breakpoint(breakPoint).md5(md5).url(url).tag(tag).enqueue(callback)
        }
    
    }
     ```
     * 定义 TKState 类,用于将网络回调转化为 LiveData
    
     ```kotlin
     /**
      *将 HttpCallback 回调转化为对应的 LiveData
     */
    class TKState<T> {
    
        var state: Int = 0
        var code = TKErrorCode.ERRCODE_UNKNOWN
        var msg: String? = null
        var data: T? = null
        var progress: Long = 0
        var total: Long = 0
    
        @JvmOverloads
        constructor(state: Int, data: T? = null, msg: String? = "") {
            this.state = state
            this.data = data
            this.msg = msg
        }
    
        constructor(state: Int, throwable: Throwable?) {
            this.state = state
            handleThrowable(throwable).run {
                this@TKState.code = first
                this@TKState.msg = second
            }
        }
    
        constructor(state: Int, progress: Long, total: Long) {
            this.state = state
            this.progress = progress
            this.total = total
        }
    
        fun handle(handleCallback: HandleCallback<T>.() -> Unit) {
            val callback = HandleCallback<T>()
            callback.apply(handleCallback)
            when (state) {
                START -> {
                callback.onStart?.invoke()
                }
                SUCCESS -> {
                callback.onSuccess?.invoke(data)
                }
                FAIL -> {
                callback.onFailure?.invoke(code, msg)
                }
                PROGRESS -> {
                callback.onProgress?.invoke(progress, total)
                }
            }
            if (state == SUCCESS || state == FAIL) {
                callback.onComplete?.invoke()
            }
        }
    
        open class HandleCallback<T> {
            var onStart: (() -> Unit)? = null
            var onSuccess: ((T?) -> Unit)? = null
            var onFailure: ((Int, String?) -> Unit)? = null
            var onComplete: (() -> Unit)? = null
            var onProgress: ((Long, Long) -> Unit)? = null
    
            fun onStart(callback: (() -> Unit)?) {
                this.onStart = callback
            }
    
            fun onSuccess(callback: ((T?) -> Unit)?) {
                 this.onSuccess = callback
            }
    
            fun onFailure(callback: ((Int, String?) -> Unit)?) {
                this.onFailure = callback
            }
    
            fun onComplete(callback: (() -> Unit)?) {
                this.onComplete = callback
            }
    
            fun onProgress(callback: ((Long, Long) -> Unit)?) {
                this.onProgress = callback
            }
        }
    
        companion object {
            const val START = 0
            const val SUCCESS = 1
            const val FAIL = 2
            const val PROGRESS = 3
    
            fun <T> start(): TKState<T> {
             return TKState(START)
            }
    
            fun <T> response(response: TKResponse<T>?): TKState<T> {
                if (response != null) {
                if (response.isSuccess()) {
                    return TKState(SUCCESS, response.data, null)
                } else {
                    return error(ServerException(response.code, response.msg))
                }
    
                } else {
                return error(NullResponseException(TKErrorCode.ERRCODE_RESPONSE_NULL, TKErrorCode.ERRCODE_RESPONSE_NULL_DESC))
                }
    
            }
    
            fun <T> error(t: Throwable?): TKState<T> {
                return TKState(FAIL, t)
            }
    
            fun <T> progress(progress: Long, total: Long): TKState<T> {
                return TKState(PROGRESS, progress, total)
            }
        }
    
    }
     ```
    
     * 经过一系列封装,参考 demo 代码,最后在 View 层如 Activity 中 ViewModel 调用 Repository 中的接口
     ```kotlin
      viewModel.testPost(hashMapOf(
                "name" to "jack",
                "location" to "shanghai",
                "age" to 28)
        )
        .observeState(this) {
            onStart {
            LogUtils.d(TAG, "onButtonPost() onStart called")
            }
            onSuccess {
            LogUtils.d(TAG, "onButtonPost() onSuccess called:${it}")
            }
            onFailure { code, msg ->
            ToastHelper.toast("onButtonPost() onFailure,code:${code},msg:${msg}")
            }
            onComplete {
            LogUtils.d(TAG, "onButtonPost() onComplete called")
            }
        }
     ```
    
     **具体使用可以参考 demo 代码,demo 中有详细的示例演示 MVVM 项目如何使用 RxHttp**
    

    强烈建议下载 Demo 代码,Demo 中有详细的示例,演示 MVVM 及 MVC 架构如何使用 RxHttp,如有问题可私信我,简书 掘金

    License

    Copyright (C) 2021 kongpf8848
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
       http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

     

    更多相关内容
  • 一直有人问我,RxHttp跟Retrofit相比有什么优势?在这,我想通过稳定性、功能性、易用性几个方面来谈谈我的想法。 首先我声明一点,RxHttp的出现,并不是要干掉谁,而是给大家多一个选择,多一个不一样的选择。 稳定...

    1、前言

    一直有人问我,RxHttp跟Retrofit相比有什么优势?在这,我想通过稳定性、功能性、易用性几个方面来谈谈我的想法。

    首先我声明一点,RxHttp的出现,并不是要干掉谁,而是给大家多一个选择,多一个不一样的选择。

    稳定性

    我一直认为Retrofit,是当下综合得分最高的选手,RxHttp也很优秀,但得分比Retrofit低那么一丢丢,那这一丢丢差在哪里呢?就差在稳定性这一块,毕竟Retrofit是全球知名的项目,github 上 37k+ star,稳定性肯定不用说,反观RxHttp,才2.6k+ star,仅在国内小有名气。

    稳定性不如Retrofit,但不代表RxHttp就不稳定,截止2020-12-27日,RxHttp在github上的提交已超过1000次,关闭的issue数量超过200个,发布的版本超过40个,虽然这些数据不能直接表明一个项目的稳定性,但也能作为一个参考,个人感觉,这对于一个仅开源1.5年的项目来说,已经非常不错了,可以说,RxHttp已经非常稳定了,有问题我都会积极修复。

    功能性

    其实功能都实现,无非是实现的方式不一样而已,这个没什么好说的,曾经看到过国内某某某网络框架,把Retrofit说的一无是处,说Retrofit这功能没有,那功能没有(实际上都有),然后把自己的说的高大上,不知道是不了解Retrofit还是故意这么干,在我看来,这种特意贬低别人,抬高自己的行为,是无耻的。

    易用性

    在易用性,个人认为,RxHttp就是神一般的存在,无论你是加密请求、上传、下载、进度监听、失败重试、动态Baseurl、自定义解析器等等任意请求场景,皆遵循请求三部曲,只要记住请求三部曲,就掌握了RxHttp精髓,写请求代码就会游刃有余,特别是对于新人来说,非常的友好,可以快速的上手。

    反观Retrofit,很多场景,我们都需要再次封装才能更好的使用,比如,文件上传/下载/进度监听等等,而且Retrofit多达20几个注解,对新人来说着实不太友好,对于老手,有时也会忘记某个注解是干嘛用的,再有就是多个注解非法在一起使用时,只有在编译期间才能给我们明确的错误,这也是我刚上手Retrofit比较头疼的一点。

    综上所述

    RxHttp易用性远胜Retrofit,但稳定性不如Retrofit,功能上都能实现,算打个平手。

    RxHttp&RxLife交流群(群号:378530627,经常会有技术交流,欢迎进群)

    本文仅介绍RxHttp + 协程的使用,更多功能请查看

    RxHttp 让你眼前一亮的Http请求框架

    RxHttp 完美适配Android 10/11 上传/下载/进度监听

    RxHttp 全网Http缓存最优解

    gradle依赖

    1、必选

    jitpack添加到项目的build.gradle文件中,如下:

    allprojects {
        repositories {
            maven { url "https://jitpack.io" }
        }
    }
    

    注:RxHttp 2.6.0版本起,已全面从JCenter迁移至jitpack

    //使用kapt依赖rxhttp-compiler时必须
    apply plugin: 'kotlin-kapt'
    
    android {
        //必须,java 8或更高
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    dependencies {
        implementation 'com.squareup.okhttp3:okhttp:4.9.1'
        implementation 'com.github.liujingxing.rxhttp:rxhttp:2.7.3'
        kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.7.3' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
     }
    

    2、可选

    android {
        kapt {
            arguments {
                //依赖了RxJava时,rxhttp_rxjava参数为必须,传入RxJava版本号
                arg("rxhttp_rxjava", "3.1.1")  
                arg("rxhttp_package", "rxhttp")  //指定RxHttp类包名,非必须
            }
        }
        //如果项目未集成kotlin,通过javaCompileOptions方法传参,在defaultConfig标签下
        annotationProcessorOptions {
            arguments = [
                rxhttp_rxjava: '3.1.1',
                rxhttp_package: 'rxhttp'
            ]
        }
    }
    dependencies {    
        //rxjava2   (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
        implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
        implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
        implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.2.1' //管理RxJava2生命周期,页面销毁,关闭请求
    
        //rxjava3
        implementation 'io.reactivex.rxjava3:rxjava:3.1.1'
        implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
        implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.2.1' //管理RxJava3生命周期,页面销毁,关闭请求
    
        //非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
        implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.7.3'
        implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.7.3'
        implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.7.3'
        implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.7.3'
        implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.7.3'
    }
    

    如果你现在对协程还一知半解,没有关系,那是因为你还没有找到运用场景,而网络请求正是一个很好的切入场景,本文会教你如何优雅,并且安全的开启协程,以及用协程处理多任务,用着用着你就会了。

    2、RxHttp 协程使用

    2.1、请求三部曲

    用过RxHttp的同学知道,RxHttp发送任意请求皆遵循请求三部曲,如下:

    1.jpg

    代码表示

    //Kotlin 协程
    val str = RxHttp.get("/service/...") //第一步,确定请求方式,可以选择postForm、postJson等方法
        .toStr()    //第二步,确认返回类型,这里代表返回String类型
        .await()    //第三步,使用await方法拿到返回值
        
    //RxJava
    RxHttp.get("/service/...") //第一步,确定请求方式,可以选择postForm、postJson等方法
        .asString()            //第二步,使用asXXX系列方法确定返回类型
        .subscribe(s -> {      //第三步, 订阅观察者
            //成功回调
        }, throwable -> {
            //失败回调
        });
    

    注: await()是suspend挂断方法,需要在另一个suspend方法或协程环境中调用

    协程请求三部曲详解

    • 第一步,选择get、postForm、postJson等方法来确定请求方式,随后便可通过add、addFile、addHeader等方法来添加参数、文件、请求头等信息

    • 第二步,调用toXxx系列方法来确定返回类型,常用的有toStr、toClass、toList,随后便可调用asFlow、retry、timeout、flowOn、filter、distinct、sort等30余个操作符来执行不同的业务逻辑,本文后续会一一介绍

    • 第三步,最后,只需调用await、tryAwait、awaitResult这三个中的任一操作符获取返回值即可,这一步,需要在协程环境中才能调用

    接着,如果我们要获取一个Student对象或者List<Student>集合对象等等任意数据类型,也是通过await()方法,如下:

    //Student对象
    val student = RxHttp.get("/service/...")
        .toClass<Student>()
        .await()
        
    //List<Student> 对象   
    val students = RxHttp.get("/service/...")
        .toClass<List<Student>>()
        .await()
    

    注:toClass()方法是万能的,你可以传递任意数据类型过去

    以上就是RxHttp在协程中最常规的操作,掌握请求三部曲,就掌握了RxHttp的精髓

    2.2、BaseUrl处理

    RxHttp通过@DefaultDomain、@Domain注解来配置默认域名及非默认域名,如下:

    public class Url {
    
        @DefaultDomain //通过该注解设置默认域名
        public static String BASE_URL = "https://www.wanandroid.com";
        
        // name 参数在这会生成 setDomainToGoogleIfAbsent方法,可随意指定名称
        // className 参数在这会生成RxGoogleHttp类,可随意指定名称
        @Domain(name = "Google", className = "Google")
        public static String GOOGLE = "https://www.google.com";
    }
    

    以上配置www.wanandroid.com为默认域名,www.google.com为非默认域名

    多BaseUrl处理

    //使用默认域名发请求
    RxHttp.get("/service/...")
        .toSrt().await()
       
    //使用google域名方式一:传入的url直接带上google域名
    RxHttp.get("https://wwww.google.com/service/...")
        .toSrt().await()
        
    //使用google域名方式二:调用setDomainToGoogleIfAbsent方法
    RxHttp.get("/service/...")
        .setDomainToGoogleIfAbsent()
        .toSrt().await()
     
    //使用google域名方式三:直接使用RxGoogleHttp类发送请求
    RxGoogleHttp.get("/service/...")
        .toSrt().await()
    

    注意:手动传入的域名优先级别最高,其次是调用setDomainToXxx方法,最后才会使用默认域名

    动态域名处理

    //直接对url重新赋值即可,改完立即生效
    Url.BASE_URL = "https://www.baidu.com";
    RxHttp.get("/service/...")
        .toSrt().await()
    //此时请求的url为 https://www.baidu.com/service/...
    

    2.3、业务code统一判断

    我想大部分人的接口返回格式都是这样的

    class BaseResponse<T> {
        var code = 0
        var msg : String? = null
        var data : T 
    }
    

    拿到该对象的第一步就是对code做判断,如果code != 200(假设200代表数据正确),就会拿到msg字段给用户一些错误提示,如果等于200,就拿到data字段去更新UI,常规的操作是这样的

    val response = RxHttp.get("/service/...")
        .toClass<BaseResponse<Student>>()
        .await()
    if (response.code == 200) {
        //拿到data字段(Student)刷新UI
    } else {
        //拿到msg字段给出错误提示
    } 
    

    试想一下,一个项目少说也有30+个这样的接口,如果每个接口读取这么判断,就显得不够优雅,也可以说是灾难,相信也没有人会这么干。而且对于UI来说,只需要data字段即可,错误提示啥的我管不着。

    那有没有什么办法,能直接拿到data字段,并且对code做出统一判断呢?有的,直接上代码

    val student = RxHttp.get("/service/...")
        .toResponse<Student>() //调用此方法,直接拿到data字段,也就是Student对象   
        .awaitResult {  
            val student = it
            //更新UI
        }.onFailure {
            val msg = it.msg  
            val code = it.code                            
        }
    

    可以看到,这里调用了toResponse()方法,就直接拿到了data字段,也就是Student对象。

    此时,相信很多人会有疑问,

    • 业务code哪里判断的?

    • 异常回调里的it是什么对象,为啥可以拿到msg、code字段?

    先来回答第一个问题,业务code哪里判断的?

    其实toResponse()方法并不是RxHttp内部提供的,而是用户通过自定义解析器,并用@Parser注解标注,最后由注解处理器rxhttp-compiler自动生成的,听不懂?没关系,直接看代码

    @Parser(name = "Response")
    open class ResponseParser<T> : TypeParser<T> {
        
        //以下两个构造方法是必须的
        protected constructor() : super()
        constructor(type: Type) : super(type)
    
        @Throws(IOException::class)
        override fun onParse(response: okhttp3.Response): T {
            val data: BaseResponse<T> = response.convertTo(BaseResponse::class, *types)
            val t = data.data     //获取data字段
            if (data.code != 200 || t == null) { //code不等于200,说明数据不正确,抛出异常
                throw ParseException(data.code.toString(), data.msg, response)
            }
            return t
        }
    }
    

    上面代码只需要关注两点即可,

    第一点,我们在类开头使用了@Parser注解,并为解析器取名为Response,此时rxhttp-compiler就会生成toResponse<T>()方法,命名规则为to{name}

    第二点,我们在if语句里,code != 200data == null时,就抛出ParseException异常,并带上了msg、code字段,所以我们在异常回调通过强转,就可以拿到这两个字段

    接着回答第二个问题,异常回调里的it是什么对象,为啥可以拿到msg、code字段?

    其实it就是Throwable对象,而msg、codeThrowable的扩展字段,这需要我们自己为其扩展,代码如下:

    val Throwable.code: Int
        get() =
            when (this) {
                is HttpStatusCodeException -> this.statusCode //Http状态码异常
                is ParseException -> this.errorCode.toIntOrNull() ?: -1     //业务code异常
                else -> -1
            }
    
    val Throwable.msg: String
        get() {
            return if (this is UnknownHostException) { //网络异常
                "当前无网络,请检查你的网络设置"
            } else if (
                this is SocketTimeoutException  //okhttp全局设置超时
                || this is TimeoutException     //rxjava中的timeout方法超时
                || this is TimeoutCancellationException  //协程超时
            ) {
                "连接超时,请稍后再试"
            } else if (this is ConnectException) {
                "网络不给力,请稍候重试!"
            } else if (this is HttpStatusCodeException) {               //请求失败异常
                "Http状态码异常"
            } else if (this is JsonSyntaxException) {  //请求成功,但Json语法异常,导致解析失败
                "数据解析失败,请检查数据是否正确"
            } else if (this is ParseException) {       // ParseException异常表明请求成功,但是数据不正确
                this.message ?: errorCode   //msg为空,显示code
            } else {
                "请求失败,请稍后再试"
            }
        }
    

    到这,业务code统一判断就介绍完毕,上面的代码,大部分人都可以简单修改后,直接用到自己的项目上,如ResponseParser解析器,只需要改下if语句的判断条件即可

    2.4、操作符介绍

    awaitResult 返回kotlin.Result

    awaitResult是最常用的字符,它可以处理请求成功/失败回调,如下:

    val result: Result<Student> = RxHttp
        .postForm("/service/...")
        .toClass<Student>()
        .awaitResult {  
            //请求成功,通过it拿到Student对象          
        }.onFailure { 
            //请求异常,通过it拿到Throwable对象      
        }                                                            
    

    拿到kotlin.Result对象后,随后成功/失败处理相关逻辑

    tryAwait 异常返回null

    tryAwait会在异常出现时,返回null,如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(100)      //超时时长为100毫秒                        
        .tryAwait()     //这里返回 Student? 对象,如果出现异常,那它就是null 
    

    当然,tryAwait也支持异常回调,如下

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(100)      //超时时长为100毫秒                        
        .tryAwait { //同样的,出现异常,返回null
            //这里通过it拿到Throwable对象
        }     
    

    onErrorReturn、onErrorReturnItem异常默认值

    有些情况,我们不希望请求出现异常时,直接走异常回调,此时我们就可以通过两个操作符,给出默认的值,如下:

    //根据异常给出默认值
    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(100)      //超时时长为100毫秒  
        .onErrorReturn {
            //如果时超时异常,就给出默认值,否则,抛出原异常
            return@onErrorReturn if (it is TimeoutCancellationException)
                Student()                                              
            else                                                        
                throw it                                                
        }
        .await()
        
    //只要出现异常,就返回默认值
    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(100)      //超时时长为100毫秒  
        .onErrorReturnItem(Student())
        .await()
    

    repeat 轮训请求

    repeat操作符共有3个参数,如下:

    /**
     * @param times  轮训次数,默认Long.MAX_VALUE,即一直轮训
     * @param period 轮训周期,默认0
     * @param stop   轮训终止条件,默认false,即无条件轮训times次
     */
    fun <T> IAwait<T>.repeat(
        times: Long = Long.MAX_VALUE,
        period: Long = 0,
        stop: suspend (T) -> Boolean = { false }
    )
    

    以上3个参数可随意搭配使用,用法如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .repeat(10, 1000) {            //轮训 10次,每次间隔 1s
            return it.id == 8888       //如果学生 id 是 8888,则停止轮训  
        }                                             
        .await()                     
    

    retry 失败重试

    retry操作符共有3个参数,分别是重试次数、重试周期、重试条件,如下:

    /**
     * 失败重试,该方法仅在使用协程时才有效
     * @param times  重试次数, 默认Int.MAX_VALUE 代表不断重试
     * @param period 重试周期, 默认为0, 单位: milliseconds
     * @param test   重试条件, 默认为true,只要出现异常就重试
     */
    fun retry(
        times: Int = Int.MAX_VALUE,
        period: Long = 0,
        test: suspend (Throwable) -> Boolean = { true }
    )
    

    3个参数可随意搭配使用,我们需要在网络出现异常时,重试2次,每次间隔1秒,代码如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .retry(2, 1000) {            //重试2次,每次间隔1s                 
            it is ConnectException   //如果是网络异常就重试     
        }                                             
        .await()                     
    

    timeout 超时

    OkHttp提供了全局的读、写及连接超时,有时我们也需要为某个请求设置不同的超时时长,此时就可以用到RxHttp的timeout(Long)方法,如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(3000)      //超时时长为3s                           
        .await()                       
    

    map 转换符号

    map操作符很好理解,RxJava及协程的Flow都有该操作符,功能都是一样,用于转换对象,如下:

    val student = RxHttp.postForm("/service/...")
        .toStr()
        .map { it.length }  //String转Int                        
        .tryAwait()     //这里返回 Student? 对象,即有可能为空  
    

    filter 过滤操作

    如果服务器返回列表数据,则我们可对列表进行过滤操作,如下:

    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .filter{ it.age > 20 }   //过滤年龄大于20岁的学生
        .await()                     
    

    还可以选用filterTo操作符,将过滤后的数据添加到指定列表中,如下:

    val list = mutableListOf<Student>()
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .filterTo(list){ it.age > 20 }   //过滤年龄大于20岁的学生
        .await()  //此时返回的列表对象就是我们传入的列表对象
    

    distinct 去重

    该操作符可以对服务器返回的列表,做去重操作,如下:

    //根据Student对象的hashCode去重
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .distinct()   
        .await()    
    
    //根据Student对象的id去重
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .distinctBy { it.id }
        .await()   
        
    //将去重后的数据添加到指定列表中,并且去重时,会对指定列表数据做判断
    val list = mutableListOf<Student>()
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .distinctTo(list) { it.id }
        .await()   
    

    sort 排序

    排序有sortXxx、sortedXxx两大类型操作符,区别在于sortXxx在列表内排序,排序完,返回自身,而sortedXxx在列表外排序,排序完,返回新的列表,这里只对sortXxx介绍,如下:

    //根据id顺序排序
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .sortBy { it.id }   
        .await() 
        
    //根据id、age两个字段顺序排序,id优先,其次age
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .sortBy({ it.id }, { it.age })  
        .await() 
    
    //返回两个排序对象,自行实现排序规则
    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .sortWith { student1, student2 ->             
            student1.id.compareTo(student2.id)        
        }                                             
        .await() 
    

    flowOn 指定上游所在线程

    该操作符跟Flow里面的flowOn操作符一样,用于指定上游所在线程,如下:

    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .sortBy { it.id }        //IO线程执行
        .flowOn(Dispatchers.IO)
        .distinctBy { it.id }    //Default线程执行
        .flowOn(Dispatchers.Default)
        .filter{ it.age > 20 }   //IO线程执行
        .flowOn(Dispatchers.IO)
        .flowOn(Dispatchers.Default)
        .await() 
    

    asFlow 转Flow对象

    如果你喜欢kotlin的flow流,那么asFlow 就派上用场了,如下:

    RxHttp.postForm("/service/...")
        .toList<Student>()
        .asFlow()
        .collect {       
            //这里拿到List<Student>对象         
        }                
    

    注意:使用asFlow操作符后,需要使用collect替代await操作符

    subList、take 截取列表

    subList用于截取某段列表,截取范围越界,则抛出越界异常;take用于从0开始,取n个数据,不足n个时,返回全部,如下:

    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .subList(1,10)  //截取9个数据
        .take(5)         //从9个中取前5个
        .await()               
    

    async 异步操作

    如果我们由两个请求需要并行时,就可以使用该操作符,如下:

    //同时获取两个学生信息
    suspend void initData() {
      val asyncStudent1 = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .async(this)   //this为CoroutineScope对象,这里会返回Deferred<Student>  
        
      val asyncStudent2 = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .async(this)   //this为CoroutineScope对象,这里会返回Deferred<Student>  
    
      //随后调用await方法获取对象    
      val student1 = asyncStudent1.await()
      val student2 = asyncStudent2.await()
    } 
    

    delay、startDelay 延迟

    delay操作符是请求结束后,延迟一段时间返回;而startDelay操作符则是延迟一段时间后再发送请求,如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .delay(1000)      //请求回来后,延迟1s返回                         
        .await()       
        
    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .startDelay(1000)     //延迟1s后再发送请求       
        .await()     
    

    自定义操作符

    RxHttp内置了一系列强大又好用的操作符,然而肯定满足不了所有的业务场景,此时我们就可以考虑自定义操作符

    自定义takeLast操作符

    如我们有这样一个需求,自定义需要在列表尾部取n条数据,不足n条,返回全部

    前面我们介绍了take操作符,它是从0开始,取n条数据,如果不足n条,则全部返回,来看看源码

    fun <T> IAwait<out Iterable<T>>.take(
        count: Int
    ): IAwait<List<T>> = newAwait {
        await().take(count)
    }
    

    代码解读,

    1、IAwait是一个接口,如下:

    interface IAwait<T> {
    
        suspend fun await(): T
    }
    

    该接口仅有一个await()方法,返回声明的T

    2、newAwait操作符,只是创建了一个IAwait接口的实现而已,如下:

    inline fun <T, R> IAwait<T>.newAwait(
        crossinline block: suspend IAwait<T>.() -> R
    ): IAwait<R> = object : IAwait<R> {
    
        override suspend fun await(): R {
            return this@newAwait.block()
        }
    }
    

    3、由于我们是为IAwait<out Iterable<T>>对象扩展的take方法,故在内部,我们调用await()方法它返回Iterable<T>对象,最后就执行Iterable<T>对象的扩展方法take(Int)获取从0开是的n条数据,take(Int)是系统提供的方法,源码如下:

    public fun <T> Iterable<T>.take(n: Int): List<T> {
        require(n >= 0) { "Requested element count $n is less than zero." }
        if (n == 0) return emptyList()
        if (this is Collection<T>) {
            if (n >= size) return toList()
            if (n == 1) return listOf(first())
        }
        var count = 0
        val list = ArrayList<T>(n)
        for (item in this) {
            list.add(item)
            if (++count == n)
                break
        }
        return list.optimizeReadOnlyList()
    }
    

    ok,回到前面的话题,如何自定义一个操作,实现在列表尾部取n条数据,不足n条,返回全部

    看了上面的take(int)源码,我们就可以很轻松的写出如下代码:

    fun <T> IAwait<out List<T>>.takeLast(
        count: Int
    ): IAwait<List<T>> = newAwait {
        await().takeLast(count)
    }
    

    首先,我们对IAwait<out List<T>>扩展了takeLast(Int)方法,随后调用newAwait创建了IAwait接口的实例对象,接着调用await()方法返回List<T>对象,最后调用系统为List<T>扩展的takeLast(Int)方法

    定义好了,我们就可直接使用,如下:

    val students = RxHttp.postForm("/service/...")
        .toList<Student>()
        .takeLast(5)         //取列表尾部5条数据,不足时,全部返回
        .await()               
    

    以上操作符随意搭配

    以上操作符,可随意搭配使用,但调用顺序的不同,产生的效果也不一样,这里先告诉大家,以上操作符仅会对上游代码产生影响。

    timeout及retry

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(50)
        .retry(2, 1000) { it is TimeoutCancellationException }                                  
        .await()                       
    

    以上代码,只要出现超时,就会重试,并且最多重试两次。

    但如果timeoutretry互换下位置,就不一样了,如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .retry(2, 1000) { it is TimeoutCancellationException }       
        .timeout(50)                                  
        .await()                       
    

    此时,如果50毫秒内请求没有完成,就会触发超时异常,并且直接走异常回调,不会重试。为什么会这样?原因很简单,timeout及retry操作符,仅对上游代码生效。如retry操作符,下游的异常是捕获不到的,这就是为什么timeout在retry下,超时时,重试机制没有触发的原因。

    在看timeoutstartDelay操作符

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .startDelay(2000)       
        .timeout(1000)                                  
        .await()                       
    

    以上代码,必定会触发超时异常,因为startDelay,延迟了2000毫秒,而超时时长只有1000毫秒,所以必定触发超时。 但互换下位置,又不一样了,如下:

    val student = RxHttp.postForm("/service/...")
        .toResponse<Student>()
        .timeout(1000)    
        .startDelay(2000)       
        .await()                       
    

    以上代码正常情况下,都能正确拿到返回值,为什么?原因很简单,上面说过,操作符只会对上游产生影响,下游的startDelay延迟,它是不管的,也管不到。

    3、上传/下载

    RxHttp对文件的优雅操作是与生俱来的,在协程的环境下,依然如此,没有什么比代码更具有说服力,直接上代码

    3.1、文件上传

     val result = RxHttp.postForm("/service/...")  
         .addFile("file", File("xxx/1.png"))        //添加单个文件   
         .addFiles("fileList", ArrayList<File>())    //添加多个文件
         .toResponse<String>()
         .await()                   
    

    只需要通过addFile系列方法添加File对象即可,就是这么简单粗暴,想监听上传进度,需要调用toFlow方法,并传入进度回调即可,如下:

    RxHttp.postForm("/service/...")      
        .addFile("file", File("xxx/1.png"))    
        .addFiles("fileList", ArrayList<File>())      
        .toFlow<String> {  //这里还可以选择自定义解析器对应的toFlowXxx方法   
            val process = it.progress         //已上传进度  0-100 
            val currentSize = it.currentSize  //已上传size,单位:byte
            val totalSize = it.totalSize      //要上传的总size  单位:byte
        }.catch {
            //异常回调
        }.collect {
            //成功回调
        }
    

    3.2、文件下载

    接着再来看看下载,直接贴代码

    val localPath = "sdcard//android/data/..../1.apk" 
    val path = RxHttp.get("/service/...")     
        .toDownload(localPath)  //下载需要传入本地文件路径   
        .await()  //返回本地存储路径,这里也就是localPath
    

    下载调用toDownload(String)方法,传入本地文件路径即可,要监听下载进度?也简单,如下:

    val localPath = "sdcard//android/data/..../1.apk"  
    val path = RxHttp.get("/service/...")      
        .toDownload(localPath) {       
            //it为Progress对象
            val process = it.progress        //已下载进度 0-100
            val currentSize = it.currentSize //已下载size,单位:byte
            val totalSize = it.totalSize     //要下载的总size 单位:byte                                           
        }     
        .await()
    

    看下toDownload方法完整签名

    /**
     * @param destPath 本地存储路径
     * @param append 是否追加下载,即是否断点下载
     * @param capacity 队列size,仅监听进度回调时生效
     * @param progress 进度回调
     */
    fun CallFactory.toDownload(
        destPath: String,
        append: Boolean = false,
        capacity: Int = 1,
        progress: (suspend (Progress) -> Unit)? = null
    ): Await<String>
    

    如果你需要断点下载,只需要appendtrue就可,如下:

    val localPath = "sdcard//android/data/..../1.apk"   
    val path = RxHttp.get("/service/...")             
        .toDownload(localPath, true) { 
            //it为Progress对象                                           
            val process = it.progress        //已下载进度 0-100           
            val currentSize = it.currentSize //已下size,单位:byte       
            val totalSize = it.totalSize     //要下的总size 单位:byte  
        }      
        .await()
    

    到这,RxHttp协程的基础Api基本介绍完毕,那么问题了,以上介绍的Api都依赖与协程环境,那我这么开启协程呢?亦或者说,我对协程不是很懂,你只要保证安全的前提下,告诉怎么用就行了,ok,那下面如何安全的开启一个协程,做到自动异常捕获,且页面销毁时,自动关闭协程及请求

    4、协程开启及关闭

    对于协程的开启,Jetpack库里提供了lifecycle-runtime-ktxlifecycle-viewmodel-ktx这两个框架,可以非常方便的开启协程,并在页面销毁时自动关闭协程,如下:

    // FragmentActivity、Fragment环境
    lifecycleScope.launch { 
        RxHttp.get("/server/...")      
            .toClass<Student>()  
            .awaitResult {
                //请求成功,通过it拿到Student对象
            }.onFailure {
                //请求异常,通过it拿到Throwable对象      
            }        
    }       
    // ViewModel环境
    viewModelScope.launch { 
        RxHttp.get("/server/...")      
            .toClass<Student>()  
            .awaitResult {
                //请求成功,通过it拿到Student对象
            }.onFailure {
                //请求异常,通过it拿到Throwable对象      
            }       
    }       
    

    如果想手动关闭,拿到launch方法返回的Job对象,通过该对象可以关闭协程,协程关闭,请求就会自定关闭,如下:

    //ViewModel环境
    val job = lifecycleScope.launch { 
        RxHttp.get("/server/...")      
            .toClass<Student>()  
            .awaitResult {
                //请求成功,通过it拿到Student对象
            }.onFailure {
                //请求异常,通过it拿到Throwable对象      
            }     
    } 
    //在合适的时机关闭协程
    job.cancel()
    

    5、协程多任务处理

    我们知道,协程最大的优势就是:能以看起来同步的代码,写出异步的逻辑,这使得我们可以非常优雅的实现多任务场景,比如多请求的并行/串行

    5.1、协程串行多个请求

    假设,我们有这么一种场景,首先获取Student对象,随后通过studentId获取学生的家庭成员列表,后者依赖于前者,这是典型的串行场景

    看看通过协程如何解决这个问题,如下:

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState);
            //启动协程,发送请求
            lifecycleScope.launch { sendRequest() }
        }
        
        suspend fun sendRequest() {
            //获取student对象,null的话,直接返回
            val student = getStudent() ?: return       
            //通过学生Id,查询家庭成员信息
            val personList = getFamilyPersons(student.id) 
            //拿到相关信息后,便可直接更新UI,如:
            tvName.text = student.name
        }
    
        //挂断方法,获取学生信息
        suspend fun getStudent(): Student? {
            return RxHttp.get("/service/...")
                .add("key", "value")
                .addHeader("headKey", "headValue")
                .toClass<Student>()
                .tryAwait() //tryAwait,出现异常时,返回null
        }
    
        //挂断方法,获取家庭成员信息
        suspend fun getFamilyPersons(studentId: Int): List<Person> {
            return RxHttp.get("/service/...")
                .add("studentId", "10000")
                .toClass<List<Person>>()
                .onErrorReturnItem(ArrayList())  //出现异常时,返回空的List
                .await() 
        }
    }
    

    我们重点看下协程代码块,首先通过第一个请求拿到Student对象,随后拿到studentId,发送第二个请求获取学习家庭成员列表,拿到后,便可以直接更新UI,怎么样,是不是看起来同步的代码,写出了异步的逻辑。

    串行请求中,只要其中一个请求出现异常,协程便会关闭(同时也会关闭请求),停止执行剩下的代码,接着走异常回调

    5.2、协程并行多个请求

    请求并行,在现实开发中,也是家常便饭,在一个Activity中,我们往往需要拿到多种数据来展示给用户,而这些数据,都是不同接口下发的。

    如我们有这样一个页面,顶部是横向滚动的Banner条,Banner条下面展示学习列表,此时就有两个接口,一个获取Banner条列表,一个获取学习列表,它们两个互不依赖,便可以并行执行,如下:

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState);
            //启动协程,发送请求
            lifecycleScope.launch { sendRequest(this) }
        }
        
        //当前在UI线程运行
        suspend fun sendRequest(scope: CoroutineScope) {
            val asyncBanner = getBanners(scope) //这里返回Deferred<List<Banner>>对象
            val asyncPersons = getStudents(scope) //这里返回Deferred<List<Student>>对象
            val banners = asyncBanner.tryAwait()    //这里返回List<Banner>?对象
            val students = asyncPersons.tryAwait()  //这里返回List<Student>?对象
            //开始更新UI
        }
    
        //挂断方法,获取学生信息
        suspend fun getBanners(scope: CoroutineScope): Deferred<List<Banner>> {
            return RxHttp.get("/service/...")
                .add("key", "value")
                .addHeader("headKey", "headValue")
                .toClass<List<Banner>>()
                .async(scope)  //注意这里使用async异步操作符
        }
    
        //挂断方法,获取家庭成员信息
        suspend fun getStudents(scope: CoroutineScope): Deferred<List<Student>> {
            return RxHttp.get("/service/...")
                .add("key", "value")
                .toClass<List<Student>>()
                .async(scope) //注意这里使用async异步操作符
        }
    }
    

    在上述代码的两个挂断方法中,均使用了async异步操作符,此时这两个请求就并行发送请求,随后拿到Deferred<T>对象,调用其await()方法,最终拿到Banner列表及Student列表,最后便可以直接更新UI。

    划重点

    并行跟串行一样,如果其中一个请求出现了异常,协程便会自动关闭(同时关闭请求),停止执行剩下的代码,接着走异常回调。如果想多个请求互不影响,就可以使用上面介绍的onErrorReturnonErrorReturnItem操作符,出现异常时,给出一个默认对象,又或者使用tryAwait操作符获取返回值,出现异常时,返回null,这样就不会影响其它请求的执行。

    6、原理剖析

    RxHttp使用到当下流行的注解处理器工具(Annotation Processing Tool,以下简称APT),像知名的EventbusButterKnifeDagger2Glide以及Jetpack库里非常好用Room数据库框架,都使用到了APT,它能够在编译时检索注解信息,通过Javapoet框架生成Java类、方法等相关代码(想生成Kotlin相关代码,使用kotlinpoet),并因此在运行时做到零性能损耗。

    那么,APT给RxHttp带来了哪些优势?RxHttp又是如何使用APT的?继续往下看

    说起APT,大家脑海里第一个想到的可能是解耦,没错,解耦是它的一大优势,其实它还有一个更大有优势,那就是根据配置,生成不同的代码逻辑;比如在RxHttp中,默认是不依赖RxJava的,但是如果你需要使用RxHttp + RxJava方式发送请求,就可以在annotationProcessorOptions标签中的rxhttp_rxjava参数来配置RxJava大版本,可传入RxJava2RxJava3,内部根据传入的RxJava版本,生成不同的代码,这样就可做到一套代码同时兼通RxJava2RxJava3,如果后续出了RxJava4RxJava5等新版本,一样可以兼容,而且非常简单。

    RxHttp v2.4.2以下版本中,对OkHttp的兼容,也使用了该方式去适配okhttp 各个版本,为此,RxHttp适配了OkHttp v3.12.0v4.9.0(截止2020/12/27最新版本)中的任一版本(v4.3.0除外,该版本有一个bug,导致无法适配),因此,使用RxHttp,完全不用担心okhttp版本冲突问题。

    同时兼容RxJavaOkHttp不同版本,这就是APT带给RxHttp的第一大优势。

    RxHttp是如何使用APT?在RxHttp中,一共定义了6个注解,如下:

    • @DefaultDomain:用它来指定默认的baseUrl,只能使用一次

    • @Domain:指定非默认的baseUrl,可使用多次

    • @Parser: 指定自定义的解析器,可使用多次,这个非常强大,可在解析器里写自己数据解析逻辑,并返回任意类型的数据,完美解决服务端返回的数据不规范问题

    • @Param:指定自定义的Param,可使用多次,发送统一加密请求时用到

    • @OkClient:为不同请求配置不同的OkHttpClient对象,可多次使用

    • @Converter:为不同请求配置不同的Converter对象,可多次使用

    RxHttp的注解处理器是rxhttp-compiler,它首要任务就是生成RxHttp类,其次就是检索以上6个注解,生成对应的类及方法,这就使得,无论我们如何去自定义,写请求代码时,始终遵循请求三部曲,如我们要发送统一加密的请求,就可以直接使用@Param注解生成的方法,如下:

    //发送加密的post表单请求,方法名可通过@Param注解随意指定
    val student = RxHttp.postEncryptForm("/service/...")
        .add("key", "value")
        .toClass<Student>()
        .await()
    

    其它5个注解带来的优势就不一一介绍了,总之就是另一大优势,解耦,使得任意请求,皆遵循请求三部曲

    RxHttp工作流程

    接下来,讲讲RxHttp的工作流程,有5个重要的角色,分别是:

    • RxHttp:这是最重要的一个角色,所以请求的唯一入口,内部持有一个Param对象,它的职责是,请求参数/请求头/BaseUrl的处理,请求线程的调度,提供注解生成的方法等等,最终的使命就是通过Param构建一个okhttp3.Request对象,随后在构建一个okhttp3.Call对象,并把Call对象丢给ObservableIAwait,然后由ObservableIAwait真正的去执行请求

    • Param:它的职责是处理请求参数/请求头/url等一切用来构建okhttp3.Request需要的东西,最终使命就是构建okhttp3.Request对象,它被RxHttp类所持有,RxHttp把构建okhttp3.Request对象所需要的东西,都交给Param去实现的

    • IAwiat:结合协程发请求时,真正执行网络请求的对象,具体实现类为AwaitImpl,它内部持有Parser对象,请求返回后,将okhttp3.Response丢给Parser去解析,并返回解析后的对象

    • Observable:结合RxJava发送请求时,真正执行网络请求的对象,具体实现类有ObservableCallExecuteObservableCallEnqueueObservableParser,分别用于同步请求、异步请求及请求返回后对okhttp3.Response对象的数据解析,ObservableParser内部持有Parser对象,具体的解析工作都交给Parser

    • Parser:负责数据解析工作,将数据解析成我们想要的数据类型,这是一个接口对象,内部只有onParse(response: Response): T这一个方法,具体实现类有4个: SimpleParserStreamParserSuspendStreamParserBitmapParser,第一个为万能的解析器,内部的asClass/toClss方法,就是通过它去实现的;第二第三是下载文件时用的的解析器,区别前者是结合RxJava下载的,后者是结合协程下载的;最后一个是解析Bitmap对象用的,asBitmap/toBitmap就是通过它去实现的

    工作流程图如下:

    展开全文
  • sort 排序 排序有sortXxx、sortedXxx两大类型操作...val students = RxHttp.postForm("/service/…") .toList() .sortBy { it.id } .await() //根据id、age两个字段顺序排序,id优先,其次age val students = RxHttp.po

    sort 排序

    排序有sortXxx、sortedXxx两大类型操作符,区别在于sortXxx在列表内排序,排序完,返回自身,而sortedXxx在列表外排序,排序完,返回新的列表,这里只对sortXxx介绍,如下:

    //根据id顺序排序
    val students = RxHttp.postForm("/service/…")
    .toList()
    .sortBy { it.id }
    .await()

    //根据id、age两个字段顺序排序,id优先,其次age
    val students = RxHttp.postForm("/service/…")
    .toList()
    .sortBy({ it.id }, { it.age })
    .await()

    //返回两个排序对象,自行实现排序规则
    val students = RxHttp.postForm("/service/…")
    .toList()
    .sortWith { student1, student2 ->
    student1.id.compareTo(student2.id)
    }
    .await()

    flowOn 指定上游所在线程

    该操作符跟Flow里面的flowOn操作符一样,用于指定上游所在线程,如下:

    val students = RxHttp.postForm("/service/…")
    .toList()
    .sortBy { it.id } //IO线程执行
    .flowOn(Dispatchers.IO)
    .distinctBy { it.id } //Default线程执行
    .flowOn(Dispatchers.Default)
    .filter{ it.age > 20 } //IO线程执行
    .flowOn(Dispatchers.IO)
    .flowOn(Dispatchers.Default)
    .await()

    asFlow 转Flow对象

    如果你喜欢kotlin的flow流,那么asFlow 就派上用场了,如下:

    RxHttp.postForm("/service/…")
    .toList()
    .asFlow()
    .collect {
    //这里拿到List对象
    }

    注意:使用asFlow操作符后,需要使用collect替代await操作符

    subList、take 截取列表

    subList用于截取某段列表,截取范围越界,则抛出越界异常;take用于从0开始,取n个数据,不足n个时,返回全部,如下:

    val students = RxHttp.postForm("/service/…")
    .toList()
    .subList(1,10) //截取9个数据
    .take(5) //从9个中取前5个
    .await()

    async 异步操作

    如果我们由两个请求需要并行时,就可以使用该操作符,如下:

    //同时获取两个学生信息
    suspend void initData() {
    val asyncStudent1 = RxHttp.postForm("/service/…")
    .toResponse()
    .async(this) //this为CoroutineScope对象,这里会返回Deferred

    val asyncStudent2 = RxHttp.postForm("/service/…")
    .toResponse()
    .async(this) //this为CoroutineScope对象,这里会返回Deferred

    //随后调用await方法获取对象
    val student1 = asyncStudent1.await()
    val student2 = asyncStudent2.await()
    }

    delay、startDelay 延迟

    delay操作符是请求结束后,延迟一段时间返回;而startDelay操作符则是延迟一段时间后再发送请求,如下:

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .delay(1000) //请求回来后,延迟1s返回
    .await()

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .startDelay(1000) //延迟1s后再发送请求
    .await()

    onErrorReturn、onErrorReturnItem异常默认值

    有些情况,我们不希望请求出现异常时,直接走异常回调,此时我们就可以通过两个操作符,给出默认的值,如下:

    //根据异常给出默认值
    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .timeout(100) //超时时长为100毫秒
    .onErrorReturn {
    //如果时超时异常,就给出默认值,否则,抛出原异常
    return@onErrorReturn if (it is TimeoutCancellationException)
    Student()
    else
    throw it
    }
    .await()

    //只要出现异常,就返回默认值
    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .timeout(100) //超时时长为100毫秒
    .onErrorReturnItem(Student())
    .await()

    awaitResult 返回kotlin.Result

    这个就直接看代码吧

    val result: Result = RxHttp
    .postForm("/service/…")
    .toClass()
    .awaitResult()
    if (result.isSuccess) {
    //请求成功,拿到Student对象
    val student = result.getOrThrow()
    } else {
    //请求出现异常,拿到Throwable对象
    val throwable = result.exceptionOrNull()
    }

    拿到kotlin.Result对象后,我们需要判断请求成功与否,随后在执行相关逻辑

    tryAwait 异常返回null

    tryAwait会在异常出现时,返回null,如下:

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .timeout(100) //超时时长为100毫秒
    .tryAwait() //这里返回 Student? 对象,如果出现异常,那它就是null

    自定义操作符

    RxHttp内置了一系列强大又好用的操作符,然而肯定满足不了所有的业务场景,此时我们就可以考虑自定义操作符

    自定义takeLast操作符

    如我们有这样一个需求,自定义需要在列表尾部取n条数据,不足n条,返回全部

    前面我们介绍了take操作符,它是从0开始,取n条数据,如果不足n条,则全部返回,来看看源码

    fun IAwait<out Iterable>.take(
    count: Int
    ): IAwait<List> = newAwait {
    await().take(count)
    }

    代码解读,

    1、IAwait是一个接口,如下:

    interface IAwait {

    suspend fun await(): T
    }

    该接口仅有一个await()方法,返回声明的T

    2、newAwait操作符,只是创建了一个IAwait接口的实现而已,如下:

    inline fun <T, R> IAwait.newAwait(
    crossinline block: suspend IAwait.() -> R
    ): IAwait = object : IAwait {

    override suspend fun await(): R {
    return this@newAwait.block()
    }
    }

    3、由于我们是为IAwait<out Iterable<T>>对象扩展的take方法,故在内部,我们调用await()方法它返回Iterable<T>对象,最后就执行Iterable<T>对象的扩展方法take(Int)获取从0开是的n条数据,take(Int)是系统提供的方法,源码如下:

    public fun Iterable.take(n: Int): List {
    require(n >= 0) { “Requested element count $n is less than zero.” }
    if (n == 0) return emptyList()
    if (this is Collection) {
    if (n >= size) return toList()
    if (n == 1) return listOf(first())
    }
    var count = 0
    val list = ArrayList(n)
    for (item in this) {
    list.add(item)
    if (++count == n)
    break
    }
    return list.optimizeReadOnlyList()
    }

    ok,回到前面的话题,如何自定义一个操作,实现在列表尾部取n条数据,不足n条,返回全部

    看了上面的take(int)源码,我们就可以很轻松的写出如下代码:

    fun IAwait<out List>.takeLast(
    count: Int
    ): IAwait<List> = newAwait {
    await().takeLast(count)
    }

    首先,我们对IAwait<out List<T>>扩展了takeLast(Int)方法,随后调用newAwait创建了IAwait接口的实例对象,接着调用await()方法返回List<T>对象,最后调用系统为List<T>扩展的takeLast(Int)方法

    定义好了,我们就可直接使用,如下:

    val students = RxHttp.postForm("/service/…")
    .toList()
    .takeLast(5) //取列表尾部5条数据,不足时,全部返回
    .await()

    以上操作符随意搭配

    以上操作符,可随意搭配使用,但调用顺序的不同,产生的效果也不一样,这里先告诉大家,以上操作符仅会对上游代码产生影响。

    timeout及retry

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .timeout(50)
    .retry(2, 1000) { it is TimeoutCancellationException }
    .await()

    以上代码,只要出现超时,就会重试,并且最多重试两次。

    但如果timeoutretry互换下位置,就不一样了,如下:

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .retry(2, 1000) { it is TimeoutCancellationException }
    .timeout(50)
    .await()

    此时,如果50毫秒内请求没有完成,就会触发超时异常,并且直接走异常回调,不会重试。为什么会这样?原因很简单,timeout及retry操作符,仅对上游代码生效。如retry操作符,下游的异常是捕获不到的,这就是为什么timeout在retry下,超时时,重试机制没有触发的原因。

    在看timeoutstartDelay操作符

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .startDelay(2000)
    .timeout(1000)
    .await()

    以上代码,必定会触发超时异常,因为startDelay,延迟了2000毫秒,而超时时长只有1000毫秒,所以必定触发超时。 但互换下位置,又不一样了,如下:

    val student = RxHttp.postForm("/service/…")
    .toResponse()
    .timeout(1000)
    .startDelay(2000)
    .await()

    以上代码正常情况下,都能正确拿到返回值,为什么?原因很简单,上面说过,操作符只会对上游产生影响,下游的startDelay延迟,它是不管的,也管不到。

    3、上传/下载

    RxHttp对文件的优雅操作是与生俱来的,在协程的环境下,依然如此,没有什么比代码更具有说服力,直接上代码

    3.1、文件上传

    val result = RxHttp.postForm("/service/…")
    .addFile(“file”, File(“xxx/1.png”)) //添加单个文件
    .addFile(“fileList”, ArrayList()) //添加多个文件
    .toResponse()
    .await()

    只需要通过addFile系列方法添加File对象即可,就是这么简单粗暴,想监听上传进度?简单,再加一个upload操作符即可,如下:

    val result = RxHttp.postForm("/service/…")
    .addFile(“file”, File(“xxx/1.png”))
    .addFile(“fileList”, ArrayList())
    .upload(this) { //此this为CoroutineScope对象,即当前协程对象
    //it为Progress对象
    val process = it.progress //已上传进度 0-100
    val currentSize = it.currentSize //已上传size,单位:byte
    val totalSize = it.totalSize //要上传的总size 单位:byte
    }
    .toResponse()
    .await()

    我们来看下upload方法的完整签名,如下:

    /**

    • 调用此方法监听上传进度
    • @param coroutine CoroutineScope对象,用于开启协程,回调进度,进度回调所在线程取决于协程所在线程
    • @param progress 进度回调
    • 注意:此方法仅在协程环境下才生效
      */
      fun RxHttpFormParam.upload(
      coroutine: CoroutineScope? = null,
      progress: (Progress) -> Unit
      ):RxHttpFormParam

    3.2、文件下载

    接着再来看看下载,直接贴代码

    val localPath = “sdcard//android/data/…/1.apk”
    val student = RxHttp.get("/service/…")
    .toDownload(localPath) //下载需要传入本地文件路径
    .await()

    下载调用toDownload(String)方法,传入本地文件路径即可,要监听下载进度?也简单,如下:

    val localPath = “sdcard//android/data/…/1.apk”
    val student = RxHttp.get("/service/…")
    .toDownload(localPath, this) { //此this为CoroutineScope对象
    //it为Progress对象
    val process = it.progress //已下载进度 0-100
    val currentSize = it.currentSize //已下载size,单位:byte
    val totalSize = it.totalSize //要下载的总size 单位:byte
    }
    .await()

    看下toDownload方法完整签名

    /**

    • @param destPath 本地存储路径
    • @param coroutine CoroutineScope对象,用于开启协程,回调进度,进度回调所在线程取决于协程所在线程
    • @param progress 进度回调
      */
      fun IRxHttp.toDownload(
      destPath: String,
      coroutine: CoroutineScope? = null,
      progress: (Progress) -> Unit
      ): IAwait

    如果你需要断点下载,也是可以的,一行代码的事,如下:

    val localPath = “sdcard//android/data/…/1.apk”
    val student = RxHttp.get("/service/…")
    .setRangeHeader(1000, 300000) //断点下载,设置下载起始/结束位置
    .toDownload(localPath, this) { //此this为CoroutineScope对象
    //it为Progress对象
    val process = it.progress //已下载进度 0-100
    val currentSize = it.currentSize //已下size,单位:byte
    val totalSize = it.totalSize //要下的总size 单位:byte
    }
    .await()

    老规则,看下setRangeHeader完整签名

    /**

    • 设置断点下载开始/结束位置
    • @param startIndex 断点下载开始位置
    • @param endIndex 断点下载结束位置,默认为-1,即默认结束位置为文件末尾
    • @param connectLastProgress 是否衔接上次的下载进度,该参数仅在带进度断点下载时生效
      */
      fun setRangeHeader (
      startIndex: Long,
      endIndex: Long = 0L,
      connectLastProgress: Boolean = false
      )

    到这,RxHttp协程的基础Api基本介绍完毕,那么问题了,以上介绍的Api都依赖与协程环境,那我这么开启协程呢?亦或者说,我对协程不是很懂,你只要保证安全的前提下,告诉怎么用就行了,ok,那下面如何安全的开启一个协程,做到自动异常捕获,且页面销毁时,自动关闭协程及请求

    4、协程开启及关闭

    此时就要引入本人开源的另一个库RxLife-Coroutine,用于开启/关闭协程,并自动异常捕获,依赖如下:

    implementation ‘com.ljx.rxlife:rxlife-coroutine:2.0.0’

    本文在介绍业务code统一处理的时候,我们用到rxLifeScope属性开启协程,那这个是什么类型呢?看代码

    val ViewModel.rxLifeScope: RxLifeScope
    get() {
    val scope: RxLifeScope? = this.getTag(JOB_KEY)
    if (scope != null) {
    return scope
    }
    return setTagIfAbsent(JOB_KEY, RxLifeScope())
    }

    val LifecycleOwner.rxLifeScope: RxLifeScope
    get() = lifecycle.rxLifeScope

    可以看到,我们为ViewModelLifecycleOwner都扩展了一个名为rxLifeScope的属性,类型为RxLifeScope,ViewModel相信大家都知道了,这里就简单讲一下LifecycleOwner接口,我们的Fragment及FragmentActivity都实现了LifecycleOwner接口,而我们的Activity一般继承于AppCompatActivity,而AppCompatActivity继承于FragmentActivity,所以我们在FragmentActivity/Fragment/ViewModel环境下,可以直接使用rxLifeScope开启协程,如下:

    rxLifeScope.lanuch({
    //协程代码块,运行在UI线程
    }, {
    //异常回调,协程代码块出现任何异常,都会直接走这里
    })

    通过这种方式开启的协程,会在页面销毁时,会自动关闭协程,当然,如果你的协程代码块里还有RxHttp请求的代码,协程关闭的同时,也是关闭请求,所以在这种情况下,只需要知道如何开启协程就行,其它一律不管。

    现在,我们来看下rxLifeScope.lanuch方法的完整签名

    /**

    • @param block 协程代码块,运行在UI线程
    • @param onError 异常回调,运行在UI线程
    • @param onStart 协程开始回调,运行在UI线程
    • @param onFinally 协程结束回调,不管成功/失败,都会回调,运行在UI线程
      */
      fun launch(
      block: suspend CoroutineScope.() -> Unit,
      onError: ((Throwable) -> Unit)? = null,
      onStart: (() -> Unit)? = null,
      onFinally: (() -> Unit)? = null
      ): Job

    可以看到,不仅有失败回调,还有开始及结束回调,这对于我们发请求来说,真的非常方便,如下:

    rxLifeScope.launch({
    //协程代码块
    val students = RxHttp.postJson("/service/…")
    .toResponse<List>()
    .await()
    //可以直接更新UI
    }, {
    //异常回调,这里可以拿到Throwable对象
    }, {
    //开始回调,可以开启等待弹窗
    }, {
    //结束回调,可以销毁等待弹窗
    })

    以上代码均运行在UI线程中,请求回来后,便可直接更新UI

    也许你还有疑问,我在非FragmentActivity/Fragment/ViewModel环境下,如何开启协程,又如何关闭,其实也很简单,如下:

    val job = RxLifeScope().launch({
    val students = RxHttp.postJson("/service/…")
    .toResponse<List>()
    .await()
    }, {
    //异常回调,这里可以拿到Throwable对象
    }, {
    //开始回调,可以开启等待弹窗
    }, {
    //结束回调,可以销毁等待弹窗
    })
    job.cancel() //关闭协程

    以上代码,需要注意两点,第一,我们需要手动创建RxLifeScope()对象,随后开启协程;第二,开启协程后,可以拿到Job对象,我们需要通过该对象手动关闭协程。其它就没啥区别了。

    5、协程多任务处理

    最后

    希望本文对你有所启发,有任何面试上的建议也欢迎留言分享给大家。

    好了,今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以加一下下面的技术群来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

    这里放一下资料获取方式:GitHub

    好了~如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

    今天的分享就到这里,如果你对在面试中遇到的问题,或者刚毕业及工作几年迷茫不知道该如何准备面试并突破现状提升自己,对于自己的未来还不够了解不知道给如何规划,可以加一下下面的技术群来看看同行们都是如何突破现状,怎么学习的,来吸收他们的面试以及工作经验完善自己的之后的面试计划及职业规划。

    这里放一下资料获取方式:GitHub

    [外链图片转存中…(img-oG5Nukme-1645110129191)]

    好了~如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

    [外链图片转存中…(img-XRrYDO7S-1645110129192)]

    为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

    展开全文
  • 随着Android 11的正式发布,适配Android 10/11 分区存储就更加的迫切了,因为Android 11开始,将强制开启分区存储,我们就无法再以绝对路径的方式去读写非沙盒目录下的文件,为此,RxHttp 在2.4.0版本中就正式适配了...

    1、前言

    随着Android 11的正式发布,适配Android 10/11 分区存储就更加的迫切了,因为Android 11开始,将强制开启分区存储,我们就无法再以绝对路径的方式去读写非沙盒目录下的文件,为此,RxHttp 2.4.0版本中就正式适配了分区存储,并且,可以非常优雅的实现文件上传/下载/进度监听,三步即可搞懂任意请求。

    老规矩,先看看请求三部曲

    如果你想了解RxHttp更过功能,请查看以下系列文章

    RxHttp 2000+star,协程请求,仅需三步

    RxHttp 让你眼前一亮的Http请求框架

    gradle依赖

    必须

    //使用kapt依赖rxhttp-compiler时必须
    apply plugin: 'kotlin-kapt'
    
    android {
        //必须,java 8或更高
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    dependencies {
        implementation 'com.ljx.rxhttp:rxhttp:2.5.2'
        implementation 'com.squareup.okhttp3:okhttp:4.9.0' //rxhttp v2.2.2版本起,需要手动依赖okhttp
        kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.2' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
     }
    

    可选

    android {
        defaultConfig {
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [
                        //使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava2、rxjava3
                        rxhttp_rxjava: 'rxjava3', 
                        rxhttp_package: 'rxhttp'   //非必须,指定RxHttp类包名
                    ]
                }
            }
        }
    }
    dependencies {
        implementation 'com.ljx.rxlife:rxlife-coroutine:2.0.1' //管理协程生命周期,页面销毁,关闭请求
        
        //rxjava2   (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
        implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
        implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
        implementation 'com.ljx.rxlife2:rxlife-rxjava:2.0.0' //管理RxJava2生命周期,页面销毁,关闭请求
    
        //rxjava3
        implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
        implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
        implementation 'com.ljx.rxlife3:rxlife-rxjava:3.0.0' //管理RxJava3生命周期,页面销毁,关闭请求
    
        //非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
        implementation 'com.ljx.rxhttp:converter-fastjson:2.5.2'
        implementation 'com.ljx.rxhttp:converter-jackson:2.5.2'
        implementation 'com.ljx.rxhttp:converter-moshi:2.5.2'
        implementation 'com.ljx.rxhttp:converter-protobuf:2.5.2'
        implementation 'com.ljx.rxhttp:converter-simplexml:2.5.2'
    }
    

    2、Android 10/11 分区存储

    当我们App的targetSdkVersion更改为28以上,并且运行在Android 10以上设备时,我们无法再以绝对路径的方式,去读写非沙盒目录下的文件,当然,如果App是覆盖安装(如:targetSdkVersion 28 覆盖安装为 29),则会保持原来的访问方式。

    requestLegacyExternalStorage属性

    如果我们的app将targetSdkVersion更改为28以上,且想保持原来的访问方式,则需要在清单文件中将 requestLegacyExternalStorage 的值设置为 true,如下:

    <manifest ...>
    <!-- This attribute is "false" by default on apps targeting
         Android 10 or higher. -->
       <application android:requestLegacyExternalStorage="true" ... >
       ...
       </application>
    </manifest>
    

    此时,便可继续以原来的方式去读写文件,然而,在Android 11上,Google又给了它新的含义,来看看官网的原话

    也就是说,在Android 11设备上,targetSdkVersion为29以上的app,将强制开启分区存储,requestLegacyExternalStorage属性失效

    注意,只要同时满足以上两个条件,不管是覆盖安装还是requestLegacyExternalStorage = true,都会强制开启分区存储

    分区存储优势

    • 对用户来说,解决了文件乱放的现象

    • 对于开发者来说,我们无需写权限,就可以在分区目录下创建文件,并且访问自己创建的文件,不需要读权限(访问其它应用创建的文件,还是需要读权限)

    新的文件访问方式

    此图来源于作者[连续三届村草]分享的Android 10(Q)/11® 分区存储适配一文,感谢作者的总结

    3、上传

    3.1、简单上传

    在介绍Android 10文件上传前,我们先来看看Android 10之前是如何上传文件的,如下:

    //kotlin 协程
    val result = RxHttp.postForm("/service/...")             
        .add("key", "value")
        .addFile("file", File("xxx/1.jpg"))                                   
        .awaitString()   //awaitXxx系列方法是挂断方法  
        
    //RxJava
    RxHttp.postForm("/service/...")      
        .add("key", "value")
        .addFile("file", File("xxx/1.jpg")) 
        .asString()                             
        .subscribe({                       
            //成功回调                              
        }, {                       
            //异常回调                              
        })                             
    

    以上,我们仅需调用 addFile方法添加文件对象即可,RxHttp提供了一系列addFile方法,列出几个常用的,如下:

    //添加单个文件
    addFile(String, File) 
    //添加多个文件,每个文件对应相同的key
    addFile(String, List<? extends File> fileList) 
    //添加多个文件,每个文件对应不同的key
    addFile(Map<String, ? extends File> fileMap) 
    //等等其它addFile方法
    

    在Android 10,我们需要通过Uri对象去上传文件,在RxHttp中,通过addPart方法添加Uri对象,如下:

    
    val context = getContext();  //获取上下文对象   
    //获取Uri对象,这里为了方便,随便写了一个Downlaod目录下的Uri地址
    val uri = Uri.parse("content://media/external/downloads/13417")
    
    //kotlin 协程
    val result = RxHttp.postForm("/service/...")             
        .add("key", "value")
        .addPart(context, "file", uri)                                   
        .awaitString()   //awaitXxx系列方法是挂断方法    
        
    //RxJava
    RxHttp.postForm("/service/...")        
        .add("key", "value")
        .addPart(context, "file", uri)                              
        .asString()                                                 
        .subscribe({                                           
            //成功回调                                                  
        }, {                                           
            //异常回调                                                  
        })
    

    同样的,RxHttp内部提供了一系列addPart方法供大家选择,列出几个常用的,如下:

    //添加单个Uri对象
    addPart(Context, String, Uri) 
    //添加多个Uri对象,每个Uri对应相同的key
    addParts(Context,String, List<? extends Uri> uris) 
    //添加多个Uri对象,每个Uri对应不同的key
    addParts(Context context, Map<String, ? extends Uri> uriMap) 
    //等等其它addPart方法
    

    3.2、带进度上传

    老规矩,看看Android 10之前是如何监听上传进度的,如下:

    //kotlin 协程
    val result = RxHttp.postForm("/service/...")             
        .add("key", "value")
        .addFile("file", File("xxx/1.jpg")) 
        .upload(this) {//this为当前协程CoroutineScope对象,用于控制回调线程      
            //上传进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小    
        }
        .awaitString()   //awaitXxx系列方法是挂断方法  
        
    //RxJava
    RxHttp.postForm("/service/...")      
        .add("key", "value")
        .addFile("file", File("xxx/1.jpg"))
        .upload(AndroidSchedulers.mainThread()) {            
            //上传进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小   
        }                                                               
        .asString()                             
        .subscribe({                       
            //成功回调                              
        }, {                       
            //异常回调                              
        })                   
    

    相比于单纯的上传文件,我们仅需额外调用upload操作符,传入线程调度器及进度回调即可。

    同样的,对于Andorid 10,我们仅需要将File对象换成Uri对象即可,如下:

    val context = getContext();  //获取上下文对象   
    //获取Uri对象,这里为了方便,随便写了一个Downlaod目录下的Uri地址
    val uri = Uri.parse("content://media/external/downloads/13417")
    
    //kotlin 协程
    val result = RxHttp.postForm("/service/...")             
        .add("key", "value")
        .addPart(context, "file", uri)     
        .upload(this) {//this为当前协程CoroutineScope对象,用于控制回调线程      
            //上传进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小   
        }
        .awaitString()   //awaitXxx系列方法是挂断方法     
        
    //RxJava
    RxHttp.postForm("/service/...")      
        .add("key", "value")
        .addPart(context, "file", uri)     
        .upload(AndroidSchedulers.mainThread()) {            
            //上传进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小     
        }                                                          
        .asString()                             
        .subscribe({                       
            //成功回调                              
        }, {                       
            //异常回调                              
        })                
    

    怎么样?是不是so easy!!

    4、下载

    下载较于上传,要丰富很多,RxHttp内部提供类一系列下载方法来满足不同的需求,如下:

    //kotlin
    fun IRxHttp.toDownload(
        destPath: String,
        context: CoroutineContext? = null,
        progress: (suspend (ProgressT<String>) -> Unit)? = null
    )
    fun IRxHttp.toDownload(
        context: Context,
        uri: Uri,
        coroutineContext: CoroutineContext? = null,
        progress: (suspend (ProgressT<Uri>) -> Unit)? = null
    )
    fun <T> IRxHttp.toDownload(
        osFactory: OutputStreamFactory<T>,
        context: CoroutineContext? = null,
        progress: (suspend (ProgressT<T>) -> Unit)? = null
    )
    

    4.1、简单下载

    在Android 10之前,我们仅需传入一个本地文件路径即可,如下:

    val localPath = "/sdcard/.../xxx.apk"
    //kotlin 协程
    val result = RxHttp.get("/service/.../xxx.apk")       
        .toDownload(localPath)
        .await()  //这里返回sd卡存储路径
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asDownload(localPath)
        .subscribe({                
            //成功回调,这里返回sd卡存储路径                      
        }, {                
            //异常回调                   
        })
    

    而到了Android 10,我们需要自定义一个Android10DownloadFactory类,继承UriFactory类,如下:

    class Android10DownloadFactory @JvmOverloads constructor(
        context: Context,
        fileName: String,
        queryUri: Uri? = null
    ) : UriFactory(context, queryUri, fileName) {
    
        override fun getUri(response: Response): Uri {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                ContentValues().run {
                    put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) //文件名
                    //取contentType响应头作为文件类型
                    put(MediaStore.MediaColumns.MIME_TYPE, response.body?.contentType().toString())
                    //下载到Download目录
                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                    val uri = queryUri ?: MediaStore.Downloads.EXTERNAL_CONTENT_URI
                    context.contentResolver.insert(uri, this)
                } ?: throw NullPointerException("Uri insert fail, Please change the file name")
            } else {
                Uri.fromFile(File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), displayName))
            }
        }
    }
    

    这里简单介绍下上面的代码,本文后续会详细介绍为啥定义这样一个类,以及如何构建一个Uri对象。

    • 首先就是继承UriFactory抽象类,实现getUri(Response)方法
    • 接着就在实现方法里判断SDK版本,Android 10以上,就通过contentResolver构建Uri对象,否则就根据File对象构建Uri对象,这样就可以兼容到所有系统版本
    • 最后返回Uri对象即可

    注:以上代码,基本可以满足大部分人的需求,如你有特殊需求,构建Uri的过程的作出简单的修改即可

    有了Android10DownloadFactory类,执行Android 10下载就会及其方便,如下:

    val factory = Android10DownloadFactory(context, "test.apk")
    
    //kotlin 协程
    val uri = RxHttp.get("/service/.../xxx.apk")       
        .toDownload(factory)
        .await()  //这里返回工厂类构建的Uri对象
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asDownload(factory)
        .subscribe({                
            //成功回调,这里返回工厂类构建的Uri对象    
        }, {                
            //异常失败                       
        })
    

    以上asDownloadtoDownload方法都接收一个UriFactory类型参数,故我们可以直接传入Android10DownloadFactory对象。

    4.2、带进度下载

    对于带进度下载,我们只需要调用asDownloadtoDownload方法时,传入线程调度器及进度回调即可,如下:

    Android 10之前

    val localPath = "/sdcard/.../xxx.apk"
    
    //kotlin 协程
    val result = RxHttp.get("/service/.../xxx.apk")       
        .toDownload(localPath, Dispatchers.Main) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .await()  //这里返回sd卡存储路径
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asDownload(localPath, AndroidSchedulers.mainThread()) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .subscribe({                
            //成功回调,这里返回sd卡存储路径                      
        }, {                
            //异常失败                       
        }) 
    

    Android 10以上,传入我们定义的Android10DownloadFactory对象的同时,再传入传入线程调度器及进度监听即可,如下:

    val factory = Android10DownloadFactory(context, "test.apk")
    
    //kotlin 协程
    val uri = RxHttp.get("/service/.../xxx.apk")       
        .toDownload(factory, Dispatchers.Main) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .await()  //这里返回工厂类构建的Uri对象
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asDownload(factory, AndroidSchedulers.mainThread()) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .subscribe({                
            //成功回调,这里返回工厂类构建的Uri对象                      
        }, {                
            //异常失败                       
        }) 
    

    4.3、带进度断点下载

    对于断点下载,我们需要调用一系列asAppendDownload、toAppendDownload方法,可以把它们理解为追加下载,实现如下:

    Android 10之前

    val localPath = "/sdcard/.../xxx.apk"
    
    //kotlin 协程
    val result = RxHttp.get("/service/.../xxx.apk")       
        .toAppendDownload(localPath, Dispatchers.Main) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .await()  //这里返回sd卡存储路径
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asAppendDownload(localPath, AndroidSchedulers.mainThread()) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .subscribe({                
            //成功回调,这里返回sd卡存储路径                      
        }, {                
            //异常失败                       
        }) 
    

    在Android 10上,有一点需要注意的是,我们在构建Android10DownloadFactory对象时,需要传入第三个参数queryUri,可以把它理解为要查询的文件夹,断点下载,RxHttp内部会根据文件名在指定的文件夹下查找对应的文件,得到当前文件的长度,也就是断点位置,从而告诉服务端从哪里开始下载,如下:

    val queryUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
    //在Download目录下查找test.apk文件
    val factory = Android10DownloadFactory(context, "test.apk", queryUri)
    
    //kotlin 协程
    val uri = RxHttp.get("/service/.../xxx.apk")       
        .toAppendDownload(factory, Dispatchers.Main) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .await()  //这里返回工厂类构建的Uri对象
        
    //RxJava
    RxHttp.get("/service/.../xxx.apk")       
        .asAppendDownload(factory, AndroidSchedulers.mainThread()) {
            //下载进度回调,0-100,仅在进度有更新时才会回调       
            val currentProgress = it.progress //当前进度 0-100   
            val currentSize = it.currentSize  //当前已上传的字节大小   
            val totalSize = it.totalSize      //要上传的总字节大小  
        }
        .subscribe({                
            //成功回调,这里返回工厂类构建的Uri对象              
        }, {                
            //异常失败                       
        }) 
    

    5、如何构建Uri对象?

    在上面代码中,我们自定义了Android10DownloadFactory类,其中最为关键的代码就是如何构建一个Uri对象,接下来,就教大家如何去构建一个Uri,马上开始,如下:

    public Uri getUri(Context context) {  
        ContentValues values = new ContentValues();
        //1、配置文件名                                                          
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1.jpg");
        //2、配置文件类型
        values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        //3、配置存储目录
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM); 
        //4、将配置好的对象插入到某张表中,最终得到Uri对象
        return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }                                                                                                  
    
    • 第一步,配置文件名称,这个就没啥好说的了

    • 第二步,配置文件类型,每个文件都应该有一个类型描述,这样,后续查找时,就可以根据这个类型去查找出同一类型的文件,如:查找相册,此属性是可选的,如果不配置,后续就无法根据类型查找到这个文件

    • 第三步,配置存储目录,这个是相对路径,总共有10个目录可选,如下:

      • Environment.DIRECTORY_DOCUMENTS 对应路径:/storage/emulated/0/Documents/
      • Environment.DIRECTORY_DOWNLOADS 对应路径:/storage/emulated/0/Download/
      • Environment.DIRECTORY_DCIM 对应路径:/storage/emulated/0/DCIM/
      • Environment.DIRECTORY_PICTURES 对应路径:/storage/emulated/0/Pictures/
      • Environment.DIRECTORY_MOVIES 对应路径:/storage/emulated/0/Movies/
      • Environment.DIRECTORY_ALARMS 对应路径:/storage/emulated/0/Alrams/
      • Environment.DIRECTORY_MUSIC 对应路径:/storage/emulated/0/Music/
      • Environment.DIRECTORY_NOTIFICATIONS 对应路径:/storage/emulated/0/Notifications/
      • Environment.DIRECTORY_PODCASTS 对应路径:/storage/emulated/0/Podcasts/
      • Environment.DIRECTORY_RINGTONES 对应路径:/storage/emulated/0/Ringtones/

    如果需要在以上目录下,创建子目录,则传入的时候,直接带上即可,如下

    values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM + "/RxHttp"); 
    
    • 第四步,插入到对应的表中,总共有5张表可选,如下:

      • 存储图片:MediaStore.Images.Media.EXTERNAL_CONTENT_URI
      • 存储视频:MediaStore.Video.Media.EXTERNAL_CONTENT_URI
      • 存储音频:MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
      • 存储任意文件:MediaStore.Downloads.EXTERNAL_CONTENT_URI
      • 存储任意文件:MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)

    需要特殊说明下,以上5张表中,只能存入对应文件类型的信息,如我们不能将音频文件信息,插入到MediaStore.Images.Media.EXTERNAL_CONTENT_URI图片表中,插入时,系统会直接抛出异常

    注意事项

    以上5张表中,除了对插入的文件类型有限制外,还对要插入的相对路径有限制,如,我们将一个apk文件下载/storage/emulated/0/Download/RxHttp/目录下,并插入到图片表中,如下:

    public Uri getUri(Context context) {  
        ContentValues values = new ContentValues();
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1.apk");
        values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/RxHttp"); 
        return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }         
    

    当执行到insert操作时,系统将会直接报错,报错信息如下:

    Primary directory Download not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]

    大致意思就是,Download目录不允许插入到MediaStore.Images.Media.EXTERNAL_CONTENT_URI表中,该表只允许插入DCIMPictures目录

    6、小结

    开源不易,写文章更不易,喜欢的话,还需劳烦大家给本文点个赞,可以的话,再给个star,我将感激不尽,🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏🙏

    展开全文
  • RxHttp,OKHTTP+RXJava一、二、三.zip
  • .await() if (response.code == 200) { //拿到data字段(Student)刷新UI } else { //拿到msg字段给出错误提示 } 试想一下,一个项目少说也有30+个这样的接口,如果每个接口读取这么判断,就...val student = RxHttp..
  • “test.apk”) //kotlin 协程 val uri = RxHttp.get("/service/…/xxx.apk") .toDownload(factory) .await() //这里返回工厂类构建的Uri对象 //RxJava RxHttp.get("/service/…/xxx.apk") .asDownload(factory) ....
  • 到这,有人又有疑问,前面初始化、设置公共参数都用到了HttpSender,这里发送请求又用到了HttpSender ,那么它又是承担怎么样的一个角色呢?看名字,我们可以理解为它就是一个...RxHttp 现在,我们正式放大招,标题说好
  • CacheKey在RxHttp的缓存模块中,是一个很重要的角色,内部缓存的读写、删除都是通过CacheKey来操作的。对于不相同的请求,要保证CacheKey的唯一性;对于相同的请求,要保证CacheKey的静态性,这两个特性非常重要,...
  • RxHttp 让你眼前一亮的Http请求框架

    千次阅读 2020-01-09 08:45:32
    RxHttp在今年4月份一经推出,就受到了广大Android 开发者的喜爱,截止本文发表在github上已有800+star,为此,我自己也建个RxHttp&RxLife 的群(群号:290611780) 目前群里也有将近120号人,里面有不少...
  • 本文由 不怕天黑 授权投稿原文链接:https://juejin.im/post/5cbd267fe51d456e2b15f623简介RxHttp是基于OkHttp的二次...
  • “test.apk”) //kotlin 协程 val uri = RxHttp.get("/service/…/xxx.apk") .toDownload(factory) .await() //这里返回工厂类构建的Uri对象 //RxJava RxHttp.get("/service/…/xxx.apk") .asDownload(factory) ....
  • 1、前言 距离上一文RxHttp 让你眼前一亮的Http请求框架...也有很多人跟说我,希望RxHttp能支持协程和缓存这两大功能,这不,非常好用的缓存公共就能来了,看完本文,相信你会再一次爱上RxHttp。另外,协程也已经在路...
  • Android RxHttp在实战中的运用

    千次阅读 2020-05-14 23:44:40
    至于RxHttp是什么,在这里就不讲了,有需要的可以前往查看RxHttp详解Github讲解 本篇主要来看一下在实战中是如何运用的 1、导入相关的库文件 implementation 'com.rxjava.rxhttp:rxhttp:1.3.4' //注解处理器,生成...
  • 否则读取缓存 3、设置单个请求的缓存策略 通过Rxhttp#setCache(CacheMode)为单个请求设置单独的缓存策略,如下: RxHttp.get("/service/…") .setCacheValidTime(10 * 1000) //当前请求缓存有效期为10秒 .setCache...
  • RxHttp是基于RxJava2+Retrofit 2.9.0+OkHttp 4.9.0实现的轻量级,完美兼容MVVM架构的网络请求封装类库,小巧精致,简单易用,轻轻松松搞定网络请求。 GitHub https://github.com/kongpf8848/RxHttp 亮点 代码量...
  • 本文系 RxHttp作者不怕天黑 向本博客投稿,并授权在本站发表。 1、前言 RxHttp在今年4月份一经推出,就受到了广大Android 开发者的喜爱,截止本文发表在github上已有1100+star,为此,我自己也建个RxHttp&RxLife...
  • 对象,即有可能为空 timeout 超时 OkHttp提供了全局的读、写及连接超时,有时我们也需要为某个请求设置不同的超时时长,此时就可以用到RxHttp的timeout(Long)方法,如下: val student = RxHttp.postForm("/service...
  • 接下来我们看看RxHttp提供的最后一个解析器DownloadParser DownloadParser DownloadParser的作用是将Http返回的输入流写到文件中,即文件下载 这个好理解,就不仔细讲解了,有一点要的说的,此解析器是支持断点
  • } } 第四步:使用RxHttp解析json数据 RxHttp.postForm("要得到数据的网址") .setDomainTobasicServicesUrlIfAbsent() .add("username", username) //添加网址的参数 .add("password", password) .asResponse...
  • RxHttp是基于OkHttp的二次封装,并于RxJava做到无缝衔接,一条链就能发送一个完整的请求。主要功能如下: 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式 支持Json、DOM等任意数据解析方式,可...
  • RxHttp基本功能介绍的时候,我们看到,通过RxHttp,我们可以确定请求方式,及调用一系列`addXXX`方法去添加参数、头部、文件等信息,其实在内部就是在操作Param类,Param为我们提供类一静态方法,供我们选择具体的...
  • rxhttp使用
  • 一种通过Param+HttpSender的形式,另外一种是直接使用RxHttp类,而RxHttp类内部其实就是通过Param+HttpSender实现的,我们可以理解为RxHttp类是Param的代理类。为此,本文将详细讲解Param类。 如果还未阅读前面两篇...
  • RxHttp是基于OkHttp的二次封装,并于RxJava做到无缝衔接,一条链就能发送一个完整的请求。主要功能如下: 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式 支持Json、DOM等任意数据解析方式,可自定义...
  • implementation ‘com.squareup.okhttp3:okhttp:4.9.0’ //rxhttp v2.2.2版本起,需要手动依赖okhttp kapt ‘com.github.liujingxing.rxhttp:rxhttp-compiler:2.6.0’ //生成RxHttp类,纯Java项目,请使用annotation...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 485
精华内容 194
关键字:

RXHttp