Android mvi 第二篇


前言

思考为什么会有本篇文章?

鉴于之前的文章没有针对于mvi做详细说明才有了本篇对mvi的详细讲解其实思路比较简单就是使用Flow替换掉了livedata为什么使用Flow呢那只能说在kotln体系里面flow是强于rxjava了本人喜欢使用代码作为实际说明也有理解不足的地方欢迎大家前来指正与交流当前mvi已经基于之前的版本做了小升级将原有mvi没有状态的设计现在加入到了现有的架构中让架构更加灵活同时flow貌似有些小改动在我之前使用其他人的例子出现api过时的情况这里做出了调整但未来可能当前使用的api还是会过时这需要大家自行调整了。


一、插个题外话如何将java异步回调编程协程异步

举个retrofit的例子retrofit使用的是java可最新版本中已经支持使用协程了这就让人很费解同时这样做的好处是只要同步回调都可以变成协程来调用极大的减少代码量。

代码如下如下示例来源

suspend fun <T : Any> Call<T>.await(): T {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        if (response.isSuccessful) {
          val body = response.body()
          if (body == null) {
    		// 省略部分代码
            continuation.resumeWithException(e)
          } else {
          	// 返回数据
            continuation.resume(body)
          }
        } else {
          continuation.resumeWithException(HttpException(response))
        }
      }
      override fun onFailure(call: Call<T>, t: Throwable) {
      	// 返回异常
        continuation.resumeWithException(t)
      }
    })
  }
}

二、说到mvi的核心那就是view_model和flow拓展下面一一讲解

1. 定义通用接口接口包含了‘状态’、‘事件’、‘单次加载状态’

代码如下示例

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

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

/**
 * 单次状态即不是持久状态类似于 EventBus ,例如加载错误提示出错、或者跳转到登录页它们只执行一次通常在 Compose 的副作用中使用
 */
interface IUiEffect

2. 封装ViewModel

进行简化后的ViewModel并不复杂对外部提供的调用和对子类提供的一目了然外部主要发送指令、接收回调、接收请求状态回调(可选)当然回调本身只会是成功的回调如有异常可能需要在接收请求状态中来进行处理

代码如下示例

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, UiEffect : IUiEffect> :
    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()

    // 状态
    private val _uiEffect: Channel<UiEffect> = Channel()
    val uiEffect: Flow<UiEffect> = _uiEffect.receiveAsFlow()

    init {
        // 初始化
        viewModelScope.launch {
            uiEvent.collect {// flow.collect 接受数据
                handleEvent(_uiState.value, it)
            }
        }
    }

    /**
     * 配置响应数据, 表示接受到数据后需要更新ui
     */
    protected abstract fun initialState(): UiState

    /**
     * 处理响应
     */
    protected abstract suspend fun handleEvent(state: UiState, event: UiEvent)


    /**
     * 通知数据流改变状态
     */
    protected fun sendState(copy: UiState.() -> UiState) {
        _uiState.update { copy(_uiState.value) }
    }

    /**
     * 发送事件, 外部调用
     */
    fun sendEvent(event: UiEvent) {
        viewModelScope.launch {
            _uiEvent.send(event)
        }
    }

    /**
     * 发送状态
     */
    protected fun sendEffect(effect: UiEffect) {
        viewModelScope.launch { _uiEffect.send(effect) }
    }
}

3.封装LoadViewModel

上面封装的ViewModel可以直接使用但使用就会比较繁琐而且每次都要写状态处理可能有些请求本身就是需要忽略或者不需要使用状态是不是可以一些通用代码放在一个地方处理呢这里依旧只做通用封装可以在LoadViewModel在做一次针对于项目异常的处理封装废话不多说查看以下步骤:

* 封装通用的数据处理类

为什么需要对数据类做抽象呢理由很简单我不希望来一个不同的数据结构项目框架就无法支持很多mvi示例都是提供一个固定的数据类如果切换项目就改动数据类即可这种构想我无法赞同所以抽象出通用数据类使用方式会放在下方。

代码如下示例

/**
 * 基础数据集, 该类尽量固定
 */
abstract class BaseData<T> {
    /**
     * 适用于当前请求是否成功, 子类必须要重写
     */
    abstract fun isSuccess(): Boolean

    /**
     * 用于返回实际数据
     */
    abstract fun data(): T?

    /**
     * 可以是业务错误, 也可以是http状态码
     */
    abstract fun errCode(): Int?
    /**
     * 请求成功但返回失败
     */
    abstract fun errMsg(): String?
}

* 定义通用加载事件

代码如下示例

/**
 * 统一加载动画事件
 */
sealed interface LoadingEffect : IUiEffect {
    /**
     * 用于判断是否需要显示加载动画
     */
    data class IsLoading(val show: Boolean) : LoadingEffect

    /**
     * 如果http状态是401则会触发该函数
     */
    data class OnAuthority(val code: Int) : LoadingEffect
}

* 封装LoadViewModel

代码如下示例

import android.net.ParseException
import com.google.gson.JsonParseException
import org.fyc.rock.lib.utils.LogUtils
import org.fyc.rock.lib.utils.ToastUtils
import org.json.JSONException
import retrofit2.HttpException
import java.io.InterruptedIOException
import java.net.ConnectException

/**
 * 带有默认事件处理的ViewModel
 */
abstract class LoadViewModel<UiState : IUiState, UiEvent : IUiEvent> :
    BaseViewModel<UiState, UiEvent, LoadingEffect>() {

    /**
     * 用于处理 http 请求, http请求框架可以替换只要满足使用的协程即可, 如果更换框架异常状态可能需要调整
     */
    protected suspend fun <T: Any> httpRequest(
        isLoadingStart: Boolean, // 是否开始加载动画, 如果多次加载可能会访问多次
        isLoadingClone: Boolean, // 是否结束加载动画
        request: suspend () -> BaseData<T>,
        onSuccess: (T?) -> Unit // 请求成功后的处理
    ) {
        if (isLoadingStart) { // 如果有多个请求需要在第一个请求中开启, 如果不需要加载动画可以忽略
            sendEffect(LoadingEffect.IsLoading(true))
        }
        try {
            val body = request()
            if (body.isSuccess()) { // 请求成功
                onSuccess(body.data())
            } else { // 请求失败
                httpFail(body.errCode()) {
                    sendEffect(LoadingEffect.OnAuthority(it)) // 如果遇到需要登录请求通知其它请求
                }
            }
        } catch (e: Exception) {
            LogUtils.e(" error = $e")
            httpError(e) {
                sendEffect(LoadingEffect.OnAuthority(it))
            }
        } finally {
            // 不管请求是否成功, 最终都需要关闭dialog加载动画
            if (isLoadingClone) {
                sendEffect(LoadingEffect.IsLoading(false)) // 通知dialog关闭
            }
        }
    }

    /**
     * http 统一异常处理
     */
    private suspend fun httpError(e: Exception, onAuthority: suspend (Int) -> Unit) =
        when (e) {
            is HttpException -> { // 请求异常
                httpFail(e.code(), onAuthority)
            }

            is ConnectException -> ToastUtils.showShort("当前无网络连接,请连接网络后再试")
            is InterruptedIOException ->
                ToastUtils.showShort("当前连接超时,请检查网络是否可用")

            is JsonParseException, is JSONException, is ParseException ->
                ToastUtils.showShort("数据解析错误,请稍后再试!")

            else -> ToastUtils.showShort("未知异常")
        }

    /**
     * http 统一错误处理
     */
    private suspend fun httpFail(errCode: Int?, onAuthority: suspend (Int) -> Unit) =
        errCode?.let {
            when (it) {
                400 -> ToastUtils.showShort("请求错误")
                401 -> onAuthority(it)
                404 -> ToastUtils.showShort("无法找到服务器")
                403 -> ToastUtils.showShort("您还没有权限访问该功能")
                500 -> ToastUtils.showShort("服务器异常")
                else -> diyHttpFail(errCode, onAuthority)
            }
        }

    /**
     * 自定义异常, 子类可以自定义
     */
    open fun diyHttpFail(errCode: Int?, onAuthority: suspend (Int) -> Unit) =
        ToastUtils.showShort("网络错误")
}

4. 定义拓展

主要用于简化调用

代码如下示例

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.launch

object BaseViewModelExt {

    /**
     * 简化状态调用
     */
    fun <S : IUiState, E : IUiEvent, F : IUiEffect> BaseViewModel<S, E, F>.collectSideEffect(
        lifecycleOwner: LifecycleOwner,
        lifecycleState: Lifecycle.State = Lifecycle.State.STARTED,
        sideEffect: (suspend (sideEffect: F) -> Unit),
    ): Job = lifecycleOwner.lifecycleScope.launch {
        uiEffect.flowWithLifecycle(lifecycleOwner.lifecycle, lifecycleState)
            .collect { sideEffect(it) }
    }

    /**
     * 拓展 Flow的使用, 用于替代, 同时将flow需要协程作用域提取出来, 以同步方式对外直接调用
     *      lifecycleScope.launchWhenStarted { } // 不要使用这种过时的方式
     */
    fun <T> Flow<T>.collectIn(
        lifecycleOwner: LifecycleOwner,
        minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
        collector: FlowCollector<T>
    ): Job = lifecycleOwner.lifecycleScope.launch {
        // 必须在协程的作用域里面
        flowWithLifecycle(lifecycleOwner.lifecycle, minActiveState)
            .collect(collector)
    }
}

5. 使用

* 定义Response

代码如下示例

interface IHomeResponse {

    /**
     * 测试代码用于演示可以使用wanandroid api测试
     */
    @GET("/xxx")
    suspend fun getUserInfo(): ResultData<String>
}

* 定义Contract

代码如下示例

/**
 * 状态管理
 */
internal data class HomeState(
    val test: TestState
): IUiState

internal sealed class TestState {
    object INIT : TestState()
    // TODO 注意这里是的body是无效代码gson无法这样转换
    // 这里主要是为了省事需要替换成实际返回类型当然要是你接口的data返回就是string那可以忽略
    data class SUCCESS(val body: String?) : TestState()
}

/**
 * 事件
 */
internal sealed interface HomeStateEvent : IUiEvent {
    object Test : HomeStateEvent
}

* 定义ViewModel

代码如下示例

/**
 * 处理请求
 */
internal class HomeViewModel(private val response: IHomeResponse) :
    LoadViewModel<HomeState, HomeStateEvent>() {

    /**
     * 初始化状态管理
     */
    override fun initialState(): HomeState =
        HomeState(TestState.INIT)


    override suspend fun handleEvent(state: HomeState, event: HomeStateEvent) = when (event) {
        HomeStateEvent.Test -> getUserInfo()
    }

    /**
     * http - 获取 user info
     * 设计两个状态值主要考虑到可能会出现a -> b -> c 这种情况
     * 为了方式状态冲突所以设定两个如果觉得麻烦可以只用一个,
     * 这里主要是演示就不监听状态了关于状态的监听在
     * ‘Android使用多模块+MVI+Koin+Flow构建项目框架’这篇文章的示例中有相同说明
     */
    private suspend fun getUserInfo() {
        httpRequest(
            isLoadingStart = false, 
            isLoadingClone = false,
            request = { response.getUserInfo() },
            onSuccess = { body ->
            	// 这里的对象一定要copy, 保证状态的唯一性
                sendState{ copy(test = TestState.SUCCESS(body))
            } })
    }
}

* 具体调用

代码如下示例

	// 监听网络请求回调
    private fun initObserve() {
    	// 这里的map { it.test } 表示监听哪个请求的如果的多请求写多个监听就可以了
    	// 如: viewModel.uiState.map { it.a } viewModel.uiState.map { it.b } ...
        viewModel.uiState.map { it.test }
            .collectIn(this, Lifecycle.State.STARTED) { uiState ->
                when(uiState) {
                    TestState.INIT -> {}
                    is TestState.SUCCESS -> {
                        logErr("请求成功 >>>>> success")
                        // 这里的body就是我们之前定义状态中的具体数据当然是可空的
                        val body = uiState.body 
                        if (body != null) {
                            logErr(">>>>>>>>>> body = $body")
                        } else {
                            logErr(">>>>>>>>>> http request error")
                        }
                    }
                }
            }
    }
    private fun initListener() {
    	setOnClickListener {
    		// 发送http请求当然数据库请求也可以
            viewModel.sendEvent(HomeStateEvent.Test)
        }
    }

三. 提供一个使用flow定义的轻量级eventbus

1. 定义数据类

代码如下示例

/**
 * 定义一个抽象类型子类需要实现
 */
abstract class FlowEvent

2. 封装类

代码如下示例

import androidx.lifecycle.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import java.util.concurrent.ConcurrentHashMap

object FlowEventBus {

    //用HashMap存储SharedFlow
    private val flowEvents = ConcurrentHashMap<String, MutableSharedFlow<FlowEvent>>()

    //获取Flow当相应Flow不存在时创建 内部调用函数
    fun getFlow(key: String): MutableSharedFlow<FlowEvent> {
        return flowEvents[key] ?: MutableSharedFlow<FlowEvent>().also { flowEvents[key] = it }
    }

    // 发送事件
    fun post(event: FlowEvent, delay: Long = 0) {
        MainScope().launch {
            delay(delay)
            getFlow(event.javaClass.simpleName).emit(event)
        }
    }

    // 订阅事件
    inline fun <reified T : FlowEvent> observe(
        lifecycleOwner: LifecycleOwner,
        minState: Lifecycle.State = Lifecycle.State.CREATED,
        dispatcher: CoroutineDispatcher = Dispatchers.Main,
        crossinline onReceived: (T) -> Unit
    ) = lifecycleOwner.lifecycleScope.launch(dispatcher) {
        getFlow(T::class.java.simpleName).collect {
            lifecycleOwner.lifecycle.withStateAtLeast(minState) {
                if (it is T) onReceived(it)
            }
        }
    }
}

3. 使用

代码如下示例

sealed class CameraEvent: FlowEvent() {
    data class PhotoPath(val msg: String) : CameraEvent()
}

fun test() {
	// 发送数据
	FlowEventBus.post(CameraEvent.PhotoPath("hello world"))
	// 监听返回
	FlowEventBus.observe<CameraEvent.PhotoPath>(this) {
	    logErr(">>>>>>>>>> TestMainActivity接收到数据: path = $it")
	    hideCamera()
	}
}

总结

这篇文章主要是补全"Android使用多模块+MVI+Koin+Flow构建项目框架"这篇文章中对mvi的介绍那篇文章写的时机不太对刚好是我这边架构以及做了升级而那篇文章中提供的demo还是比较老的部分本来想着上传一个最新的lib作为文章的解释但是确实有点没头没尾的感觉所以这篇文章是对之前的文章的补全如果没看过之前的文章看这篇可以直接拿到最新的mvi框架如果有不足的地方欢迎指出如果需要代码示例可以下载上一篇文中中的demo但是我相信动手能力强一点的应该是复刻下来没问题的至于log打印工具也是在上一篇文章中的demo可以查看链接一致就不重复提供了本人经验有限可能有认知不足的地方欢迎指正。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: android