【Jetpack篇】协程+Retrofit网络请求状态封装实战(2)

一、前言

前几天发布了一篇【Jetpack篇】协程+Retrofit网络请求状态封装实战,在评论区里也收到了一些同僚的反馈:

image.png
image.png

......

具体问题可以直接移步到上一篇评论区查看。

因为有几个问题点还蛮重要,所以就上一篇文章新增了一些内容,主要如下:

  • ✅ 新增局部状态管理。如同一个页面多个接口,可以分别管理状态切换;
  • ✅ UI层新增Error,Empty,Success的Callback,开发者可以自由选择是否监听,处理业务逻辑更直观、方便;
  • ✅ 结合第三方库loadSir,统一切换UI。
  • ✅ 请求调用更加简单

好了,正文开始。

二、局部请求状态管理

很多时候app开发,存在同一个界面不同接口的情况,两个接口同时请求,一个成功一个失败,这个时候成功接口继续显示自己的页面,失败接口则显示Error提示界面,如下图

image.png

上一篇的封装是将errorLiveData和loadingLiveData全局封装在BaseFragment中,而他们的创建也是在BaseViewModel中,这样就导致多个接口同时请求时,如果某个接口发送错误,就无法区分错误来自哪里。

如果需要每个接口单独管理自己的状态,那么就需要在ViewModel中创建多个erroeLiveData,这样问题是可以解决,但是会导致代码非常冗余。既然需要每个接口管理不同状态,那就可以新建一个既包含请求返回结果又包含不同状态值的LiveData,将之命名为StateLiveData

/**
 * MutableLiveData,用于将请求状态分发给UI
 */
class StateLiveData<T> : MutableLiveData<BaseResp<T>>() {
}

而BaseResp中除了请求返回值的公共json外,还需要添加上不同的状态值,我们将状态值分为( STATE_CREATE,STATE_LOADING,STATE_SUCCESS,STATE_COMPLETED,STATE_EMPTY,STATE_FAILED, STATE_ERROR,STATE_UNKNOWN)几种

enum class DataState {
    STATE_CREATE,//创建
    STATE_LOADING,//加载中
    STATE_SUCCESS,//成功
    STATE_COMPLETED,//完成
    STATE_EMPTY,//数据为null
    STATE_FAILED,//接口请求成功但是服务器返回error
    STATE_ERROR,//请求失败
    STATE_UNKNOWN//未知
}

将DataState添加到BaseResp中,

/**
 * json返回的基本类型
 */
class BaseResp<T>{
    var errorCode = -1
    var errorMsg: String? = null
    var data: T? = null
        private set
    var dataState: DataState? = null
    var error: Throwable? = null
    val isSuccess: Boolean
        get() = errorCode == 0
}

那StateLiveData该如何使用呢?

我们都知道数据请求会有不同的结果,成功,异常或者数据为null,那么就可以利用不同的结果,将相应的状态设置在BaseResp的DataState中。直接进入到数据请求Repository层,对上篇异常处理做了改进。

open class BaseRepository {
    /**
     * repo 请求数据的公共方法,
     * 在不同状态下先设置 baseResp.dataState的值,最后将dataState 的状态通知给UI
     */
    suspend fun <T : Any> executeResp(
        block: suspend () -> BaseResp<T>,
        stateLiveData: StateLiveData<T>
    ) {
        var baseResp = BaseResp<T>()
        try {
            baseResp.dataState = DataState.STATE_LOADING
            //开始请求数据
            val invoke = block.invoke()
            //将结果复制给baseResp
            baseResp = invoke
            if (baseResp.errorCode == 0) {
                //请求成功,判断数据是否为空,
                //因为数据有多种类型,需要自己设置类型进行判断
                if (baseResp.data == null || baseResp.data is List<*> && (baseResp.data as List<*>).size == 0) {
                    //TODO: 数据为空,结构变化时需要修改判空条件
                    baseResp.dataState = DataState.STATE_EMPTY
                } else {
                    //请求成功并且数据为空的情况下,为STATE_SUCCESS
                    baseResp.dataState = DataState.STATE_SUCCESS
                }

            } else {
                //服务器请求错误
                baseResp.dataState = DataState.STATE_FAILED
            }
        } catch (e: Exception) {
            //非后台返回错误,捕获到的异常
            baseResp.dataState = DataState.STATE_ERROR
            baseResp.error = e
        } finally {
            stateLiveData.postValue(baseResp)
        }
    }
}

executeResp()为数据请求的公共方法,该方法传入了两个参数,第一个是将数据请求函数当作参数,第二个就是上面新建的StateLiveData

方法一开始就新建了一个BaseResp<T>()对象,将DataState.STATE_LOADING状态设置给BaseResp的dataState,接着开始对数据请求进行异常处理(具体可查看上一篇),如果code=0表示接口请求成功,否则表示接口请求成功,服务器返回错误。在code=0时,对返回数据进行判空处理,因为数据有多种类型,这里需要自己设置类型进行判断,为空就将状态设置为DataState.STATE_EMPTY,否则为

DataState.STATE_SUCCESS。如果抛出异常,则将状态设置为DataState.STATE_ERROR,在请求结束后,利用stateLiveData将带有状态的baseResp分发给UI。

到这里,请求状态都设置完成,接下来只需要根据不同状态,开始进行界面切换处理。

三、结合LoadSir界面切换

LoadSir是一个加载反馈页管理框架,状态页自动切换,具体使用在这里就不描述了,需要的可移步github查看。

LiveData接收数据变化时,UI会先注册一个接收事件的观察者,接收到请求的数据后就进行UI更新,第二节里将不同状态也添加到了数据中,要想对状态也进行监听的话,就需要对Observer进行状态处理。

/**
 * LiveData Observer的一个类,
 * 主要结合LoadSir,根据BaseResp里面的State分别加载不同的UI,如Loading,Error
 * 同时重写onChanged回调,分为onDataChange,onDataEmpty,onError,
 * 开发者可以在UI层,每个接口请求时,直接创建IStateObserver,重写相应callback。
 */
abstract class IStateObserver<T>(view: View?) : Observer<BaseResp<T>>, Callback.OnReloadListener {
    private var mLoadService: LoadService<Any>? = null

    init {
        if (view != null) {
            mLoadService = LoadSir.getDefault().register(view, this,
                Convertor<BaseResp<T>> { t ->
                    var resultCode: Class<out Callback> = SuccessCallback::class.java

                    when (t?.dataState) {

                        //数据刚开始请求,loading
                        DataState.STATE_CREATE, DataState.STATE_LOADING -> resultCode =
                            LoadingCallback::class.java
                        //请求成功
                        DataState.STATE_SUCCESS -> resultCode = SuccessCallback::class.java
                        //数据为空
                        DataState.STATE_EMPTY -> resultCode =
                            EmptyCallback::class.java
                        DataState.STATE_FAILED ,DataState.STATE_ERROR -> {
                            val error: Throwable? = t.error
                            onError(error)
                            //可以根据不同的错误类型,设置错误界面时的UI
                            if (error is HttpException) {
                                //网络错误
                            } else if (error is ConnectException) {
                                //无网络连接
                            } else if (error is InterruptedIOException) {
                                //连接超时
                            } else if (error is JsonParseException
                                || error is JSONException
                                || error is ParseException
                            ) {
                                //解析错误
                            } else {
                                //未知错误
                            }
                            resultCode = ErrorCallback::class.java
                        }
                        DataState.STATE_COMPLETED, DataState.STATE_UNKNOWN -> {
                        }
                        else -> {
                        }
                    }
                    Log.d(TAG, "resultCode :$resultCode ")
                    resultCode
                })
        }

    }


    override fun onChanged(t: BaseResp<T>) {
        Log.d(TAG, "onChanged: ${t.dataState}")

        when (t.dataState) {
            DataState.STATE_SUCCESS -> {
                //请求成功,数据不为null
                onDataChange(t.data)
            }

            DataState.STATE_EMPTY -> {
                //数据为空
                onDataEmpty()
            }

            DataState.STATE_FAILED,DataState.STATE_ERROR->{
                //请求错误
                t.error?.let { onError(it) }
            }
            else -> { }
        }

        //加载不同状态界面
        Log.d(TAG, "onChanged: mLoadService $mLoadService")

        mLoadService?.showWithConvertor(t)

    }

    /**
     * 请求数据且数据不为空
     */
    open fun onDataChange(data: T?) {

    }

    /**
     * 请求成功,但数据为空
     */
    open fun onDataEmpty() {

    }

    /**
     * 请求错误
     */
    open fun onError(e: Throwable?) {

    }
}

IStateObserverObserver接口的实现类,参数传入了一个View,而这个View就是你所要替换的界面,这也就是同个界面,不同模块显示异常不同的关键所在。因为是结合Loadsir,首先需要初始化LoadService,再者通过dataState的状态值,设置不同的Callback,例如Loading时,设置为LoadingCallback,Error时,设置为ErrorCallback,Empty时设置为EmptyCallback,设置完成后,在onChanged回调中统一调用showWithConvertor,也就是切换界面的操作。

而在onChange回调中,同样根据状态值,分别分发onDataChangeonDataEmptyonError的通知。

到这里,完成了不同状态界面切换和状态通知的分发工作。

四、如何使用

上述基本上将整个流程封装完成,使用起来也相对简便。

Repository层:

class ProjectRepo() : BaseRepository() {
      suspend fun loadProjectTree(stateLiveData: StateLiveData<List<ProjectTree>>) {
        executeResp({mService.loadProjectTree()},stateLiveData)
    }
}

直接就一行代码,executeResp方法中传入api的请求,以及StateLiveData。

ViewModel层:

class ProjectViewModel : BaseViewModel() {  
    val mProjectTreeLiveData = StateLiveData<List<ProjectTree>>()
        
     fun loadProjectTree() {
        viewModelScope.launch(Dispatchers.IO) {
            mRepo.loadProjectTree(mProjectTreeLiveData)
        }
    }

调用依旧是一行代码,新建了一个StateLiveData,接着直接在viewModelScope作用域中调用Repository层的网络请求,这里记得将StateLiveData作为参数传进去。

UI层:

class ProjectFragment : BaseFragment<FragmentProjectBinding, ProjectViewModel>() {
      override fun initData() {
          
        mViewModel?.loadProjectTree()
        mViewModel?.mProjectTreeLiveData?.observe(this,
            object : IStateObserver<List<ProjectTree>>(mBinding?.rvProjectAll) {
                override fun onDataChange(data: List<ProjectTree>?) {
                    super.onDataChange(data)
                    Log.d(TAG, "onDataChange: ")
                    data?.let { mAdapter.setData(it) }
                }

                override fun onReload(v: View?) {
                    Log.d(TAG, "onReload: ")
                    mViewModel?.loadProjectTree()
                }

                override fun onDataEmpty() {
                    super.onDataEmpty()
                    Log.d(TAG, "onDataEmpty: ")
                }

                override fun onError(e: Throwable?) {
                    super.onError(e)
                    showToast(e?.message!!)
                    Log.d(TAG, "onError: ${e?.printStackTrace()}")
                }
            })
      }
}

UI层利用ViewModel的StateLiveData注册观察者,与以往不同的是,mViewModel?.mProjectTreeLiveData?.observe()第二个参数替换为了IStateObserver,并且传入了一个View,而这个View代表着的是当请求异常时,你所想替换的UI界面,同时,也多了几个回调,

  • onDataChange:请求成功,数据不为空;
  • onReload:点击重新请求;
  • onDataEmpty:数据为空时;
  • onError:请求失败

开发者可以通过自己的业务需求,自由的选择监听。

我们来看看效果。

80cc44c18c36363d3d8d589814397a48.gif

五、最后

这次的整合弥补了一些细节问题,更符合App开发逻辑,当然每个App的业务不同,这就要开发者去定制化一些请求细节,但是协程+Retrofit网络请求的大致思路就是如此。
更多详细的代码可移步至github

源码: 组件化+Jetpack+kotlin+mvvm

请结合【Jetpack篇】协程+Retrofit网络请求状态封装实战

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

推荐阅读更多精彩内容