Retrofit + Kotlin + MVVM 的网络请求框架的封装尝试之三

470 阅读3分钟

1、前言

经过博客一博客二的努力,我们的网络请求已经具备了基本的网络请求和显示加载动画和错误提示的功能。目前用着也还算顺手。不过有些小伙伴可能还是会吐槽,因为MainViewModel中代码是这样的:

class MainViewModel : ViewModel() {

    private val loginAction = MutableLiveData<Boolean>()

    val loginLiveData = loginAction.switchMap {
        if (it) {
            Repository.login("PuKxVxvMzBp2EJM")
        } else {
            Repository.login("123456")
        }
    }

    fun login() {
        loginAction.value = true
    }
}

一个登录请求,创建了两个LiveDataloginAction用于观察用户的点击操作,loginLiveData用于更新登录数据,两者分工明确。但追求简洁的人也许觉得loginAction完全可以去:用户点击按钮后直接调用Repository.login()发起网络请求就可以了,何必要拐一个弯呢?既然如此,那我们就来略作修改,将它再浓缩一下吧。

2、viewModelScope.launch的使用

首先,loginLiveData不能再通过loginAction来赋值了,它需要先初始化,然后在网络请求有结果后我们再postValue,故需要将其改成MutableLiveData

val loginLiveData = MutableLiveData<BaseResponse<LoginModel>>()

要想点击按钮就直接请求网络数据,那么就要在login()中直接调用Repository.login()了。而Repository.login()的返回值也需要改成BaseResponse。这样逐层往上修改。

首先是Repository层代码:

object Repository : BaseRepository() {

    suspend fun login(pwd: String) = fire {
        NetworkDataSource.login(pwd)
    }

}

fire()函数代码当然也要跟着更改,不再返回Live Data

    protected suspend fun <T> fire(
        block: suspend () -> BaseResponse<T>
    ): BaseResponse<T> = withContext(Dispatchers.IO) {
        var response: BaseResponse<T> = EmptyResponse() 
        kotlin.runCatching {
            block.invoke()
        }.onSuccess {
            response = when (it.success) {
                true -> checkEmptyResponse(it.data)
                false -> FailureResponse(handleException(RequestException(it)))
            }
        }.onFailure { throwable ->
            response = FailureResponse(handleException(throwable))
        }
        response
    }

注意,别忘了要将他们改成挂起函数,而且要在IO线程中执行。

Repository.login()已经是挂起函数来,那么我们在ViewModel中使用它时自然需要一个协程作用域。Android为我们准备了一个ViewModel的扩展函数:

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

用法非常简单:

viewModelScope.launch {
    loginLiveData.postValue(StartResponse())
    loginLiveData.postValue(Repository.login("PuKxVxvMzBp2EJM"))
}

至此,我们的改造就差不多完成了。

3、onStart()到哪去了?

你也许会对viewModelScope.launch中的loginLiveData.postValue(StartResponse())一行感到疑惑:为何开始请求对回调要在这里发起呢?在之前的代码里不是已经封装好了么?确实,但是别忘了,我们已经修改过BaseRepository中的fire()函数,由于采用了return的写法,是不能返回StartResponse()的,否则后面的代码没有机会执行,整个回调状态就只有onStart()了。

那么在IStateObserver中直接调用onStart()如何呢?

interface IStateObserver<T> : Observer<BaseResponse<T>> {

    override fun onChanged(response: BaseResponse<T>?) {
    		onStart()
        when (response) {
            is SuccessResponse -> onSuccess(response.data)
            is EmptyResponse -> onEmpty()
            is FailureResponse -> onFailure(response.exception)
        }
        onFinish()
    }

}

如果你这么想的话,恭喜你踩了我踩过的坑。你要注意到,onChanged()是在LiveDatavalue发生改变时才会调用,也就是说,onChanged()回调时,请求早也发起,而且已经获取到结果了(SuccessResponseEmptyResponseFailureResponse)。打日志的话可以发现onStart()onFinish()几乎是同时调用的,这在UI上的体现时加载对话框压根就不会显示出来。鉴于此,我们只能在Repository.login()调用之前先将loginLiveData的数据设置为StartResponse()

啊,那这样不又得写一堆重复代码了吗?

别急,我们手上可有着扩展函数这一利器呢。重复的代码怕什么,我们照样给它封装得舒舒服服的。在HttpRequestExt.kt中给MutableLiveData加上这个扩展函数:

//
fun <T> MutableLiveData<BaseResponse<T>>.request(
    viewModel: ViewModel,
    context: CoroutineContext = Dispatchers.Main,
    request: suspend () -> BaseResponse<T>
) {
    viewModel.viewModelScope.launch(context) {
        this@request.postValue(StartResponse())
        this@request.postValue(request())
    }
}

由于只需要更新LiveData,线程切回主线程就好了。接下来,你肯定知道该怎么用了:

fun login() {
    loginLiveData.request(this) {
        Repository.login("PuKxVxvMzBp2EJM")
    }
}

到这里,我们的修改就大功告成了。

4、代码下载

GitHub(注意要选择dev3.0分支)

5、参考资料