思考:能不能进一步简化?
这篇文章是对mvi第二篇的一个简化版本,本次封装解决两个问题:
- 响应类型使用 data class 后只能被响应一次
- 独立处理的响应状态,有概率导致收发不一致的bug
目前版本经过测试,有个小问题,便是如果需要每次请求都响应则必须开启加载状态,但收获是将状态和加载状态放到了一起处理,这样可以有效避免收发不一致的情况。
由于简化了状态处理,所以在定义中少了加载状态,看代码:
代码如下:
/**
* 需要展示的状态,对应 UI 需要的数据
*/
interface IUiState
/**
* 来自用户和系统的是事件,也可以说是命令
*/
interface IUiEvent
本次封装优化掉了之前的IUiEffect,这样和mvi图保持了一致
代码如下:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/**
* mvi 简化版
*/
abstract class BaseViewModel<UiState : IUiState, UiEvent: IUiEvent>: ViewModel() {
private val initialState: UiState by lazy { initialState() }
private val _uiState: MutableStateFlow<UiState> by lazy { MutableStateFlow(initialState) }
/** 对外暴露需要改变ui的控制 */
val uiState: StateFlow<UiState> by lazy { _uiState }
// 使用Channel创建数据流, Channel是消费者模式的, 保证了请求的正确性
private val _uiEvent: Channel<UiEvent> = Channel()
private val uiEvent: Flow<UiEvent> = _uiEvent.receiveAsFlow()
init {
// 初始化
viewModelScope.launch {
uiEvent.collect {// flow.collect 接受数据
handleEvent(it)
}
}
}
/**
* 配置响应数据, 表示接受到数据后需要更新ui
*/
protected abstract fun initialState(): UiState
/**
* 处理响应
*/
protected abstract suspend fun handleEvent(event: UiEvent)
/**
* 通知数据流改变状态
*/
protected fun sendState(copy: UiState.() -> UiState) {
val state = copy(_uiState.value)
_uiState.update { state }
}
/**
* 发送事件, 外部调用
*/
fun sendEvent(event: UiEvent) {
viewModelScope.launch {
_uiEvent.send(event)
}
}
}
当前loding定义了通用的处理,可自定义。
代码如下:
/**
* 请求返回的状态
*/
sealed class LoadingUIEffect {
/**
* 用于开启或关闭加载动画
*/
data class Loading(var isShow: Boolean) : LoadingUIEffect()
/**
* 用于处理项目中逻辑异常, 401为需要登录异常, 其它为自定义异常
*/
data class CodeFail(val code: Int) : LoadingUIEffect()
/**
* 出错后的回调
*/
data class Error(val msg: String?, val e: Throwable?) : LoadingUIEffect()
}
代码如下:
import android.net.ParseException
import com.fyc.example.mvitest.core.libBase.mvi.BaseData
import com.fyc.example.mvitest.core.utils.LogUtils
import com.fyc.example.mvitest.core.utils.ToastUtils
import com.google.gson.JsonParseException
import org.json.JSONException
import retrofit2.HttpException
import java.io.InterruptedIOException
import java.net.ConnectException
abstract class LoadingViewModel<UiState : IUiState, UiEvent : IUiEvent> :
BaseViewModel<UiState, UiEvent>() {
/**
* 简化请求处理
*/
protected suspend fun <T : Any> request(
isLoading: Boolean, // 是否开启加载动画
request: suspend () -> BaseData<T>, // 请求
onLoading: ((LoadingUIEffect) -> Unit)?, // 加载配置
onSuccess: (T?) -> Unit // 成功处理
) {
// 开启加载动画
if (isLoading) {
onLoading?.invoke(LoadingUIEffect.Loading(true))
}
try {
val body = request()
if (body.isSuccess()) { // 请求成功
onSuccess(body.data())
} else { // 请求失败
val errCode = body.errCode()
if (errCode != null) {
// 默认行为是遇到了errCode就返回, 可以用子类实现一个通用的实现
diyHttpFail(errCode) {
onLoading?.invoke(LoadingUIEffect.CodeFail(it))
}
} else {
defToastError("无法解析返回err_code", null)
}
}
} catch (e: Exception) {
LogUtils.e(" error = $e")
// 返回异常和401需要做的处理
httpError(e, {
onLoading?.invoke(LoadingUIEffect.CodeFail(it))
}, { msg, e ->
onLoading?.invoke(LoadingUIEffect.Error(msg, e))
defToastError(msg, e)
})
} finally {
if (isLoading) {
onLoading?.invoke(LoadingUIEffect.Loading(false))
}
}
}
/**
* http 统一异常处理
*/
private suspend fun httpError(
e: Exception,
onCodeFail: suspend (Int) -> Unit,
onError: suspend (String, Throwable?) -> Unit,
) = when (e) {
is HttpException -> { // 请求异常
httpFail(e.code(), onCodeFail, onError)
}
is ConnectException -> onError("当前无网络连接,请连接网络后再试", e)
is InterruptedIOException -> onError("当前连接超时,请检查网络是否可用", e)
is JsonParseException, is JSONException, is ParseException -> onError(
"数据解析错误,请稍后再试!",
e
)
else -> onError("未知异常!", e)
}
/**
* http 统一错误处理
*/
private suspend fun httpFail(
errCode: Int?,
onCodeFail: suspend (Int) -> Unit,
onError: suspend (String, Throwable?) -> Unit
) = errCode?.let {
when (it) {
400 -> onError("请求错误!", null)
401 -> onCodeFail(it)
404 -> onError("无法找到服务器!", null)
403 -> onError("您还没有权限访问该功能!", null)
500 -> onError("服务器异常!", null)
else -> ToastUtils.showShort("网络错误")
}
}
/**
* 自定义异常, 子类可以自定义
*/
open suspend fun diyHttpFail(errCode: Int, onCodeFail: suspend (Int) -> Unit) = onCodeFail(errCode)
/**
* 默认弹窗提示
*/
open fun defToastError(msg: String, e: Throwable?) {
ToastUtils.showShort(msg)
}
}
定义状态和事件,放在一个地方统一管理
/**
* 状态管理
*/
internal data class TestState(
val banner: BannerState
) : IUiState
/**
* 卡号获取token
*/
internal sealed class BannerState {
/** 初始化 **/
object INIT : BannerState()
/** 成功,这里数据类型是 data_class **/
data class SUCCESS(val body: List<BannerDto>) : BannerState()
/** 无数据 **/
object NoData: BannerState()
/** 加载状态可选 **/
data class LOADING(val loading: LoadingUIEffect): BannerState()
}
/**
* 事件
*/
internal sealed interface TestStateEvent : IUiEvent {
object Banner: TestStateEvent
}
定义ViewModel
internal class MainViewModel : LoadingViewModel<TestState, TestStateEvent>() {
// 这里使用了wanandroi api作为测试
private val response: ITestResponse by lazy {
RetrofitManager.getService(ITestResponse::class.java)
}
override fun initialState(): TestState =
TestState4(BannerState4.INIT)
override suspend fun handleEvent(event: TestStateEvent) = when (event) {
TestStateEvent.Banner -> banner()
}
private suspend fun banner() {
request(
isLoading = true,
request = { response.banner2() },
onLoading = { loading ->
sendState { copy(banner = BannerState.LOADING(loading)) }
},
onSuccess = { body ->
sendState {
copy(banner = if (body != null) BannerState.SUCCESS(body) else BannerState.NoData)
}
}
)
}
}
activity中的使用, 这里只提供代码片段
private fun initView() = bindingRun {
viewModel.sendEvent(TestStateEvent.Banner)
}
private fun initObserve() {
viewModel4.uiState.map { it.banner }
// 这个拓展可以在上一篇文章中查看,这个是通用的就不在这里继续写一遍了
.collectIn(this, Lifecycle.State.STARTED) { uiState ->
when (uiState) {
// 这个不用管
BannerState4.INIT -> {}
// 这里的处理可选, 只要在loading_view_model中开启 isLoading = true即可
is BannerState.LOADING -> {
when(val loading = uiState.loading) {
// 异常状态
is LoadingUIEffect.Error -> LogUtils.e("BannerState4.LOADING: 错误: ${loading.msg}, ${loading.e}")
// 加载状态
is LoadingUIEffect.Loading -> LogUtils.e("BannerState4.LOADING: 当前加载状态: ${loading.isShow}")
// 401等逻辑异常状态,wanandroid的是-1001, 这个可内部处理也可以外部处理
is LoadingUIEffect.CodeFail -> LogUtils.e("BannerState4.LOADING: 需要登录, code = ${loading.code}")
}
}
// 响应成功,有值
is BannerState.SUCCESS -> {
val body = uiState.body
LogUtils.e("body = $body")
}
// 响应成功,无值
BannerState.NoData -> {
LogUtils.e("body = 无数据")
}
}
}
}
当前版本将请求状态和状态本身绑定在一起,带来的便利是简化了代码量,同时也可以很清晰的知道,哪个请求对应哪个请求状态,同时将上个版本中的BannerState.SUCCESS细分成了有值返回和无值返回,这样让代码更加清晰,本次重构可能依旧具备继续完善的能力,欢迎大家提供思路。