Android 关于mvi框架优化(第三篇)

目录

  • 前言
  • 一、先上开胃菜
  • 二、base_model的封装
  • 三、定义loading
    • 1. LoadingUIEffect
    • 2. LoadingViewModel
  • 四、使用
  • 总结


前言

思考:能不能进一步简化?

这篇文章是对mvi第二篇的一个简化版本,本次封装解决两个问题:

  1. 响应类型使用 data class 后只能被响应一次
  2. 独立处理的响应状态,有概率导致收发不一致的bug

目前版本经过测试,有个小问题,便是如果需要每次请求都响应则必须开启加载状态,但收获是将状态和加载状态放到了一起处理,这样可以有效避免收发不一致的情况。


一、先上开胃菜

由于简化了状态处理,所以在定义中少了加载状态,看代码:

代码如下:

/**
 * 需要展示的状态,对应 UI 需要的数据
 */
interface IUiState

/**
 * 来自用户和系统的是事件,也可以说是命令
 */
interface IUiEvent

二、base_model的封装

本次封装优化掉了之前的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)
        }
    }
}

三、定义loading

当前loding定义了通用的处理,可自定义。

1. LoadingUIEffect

代码如下:

/**
 * 请求返回的状态
 */
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()
}

2. LoadingViewModel

代码如下:

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细分成了有值返回和无值返回,这样让代码更加清晰,本次重构可能依旧具备继续完善的能力,欢迎大家提供思路。

你可能感兴趣的:(android,mvi,升级)