思考:为什么会有本篇文章?
鉴于之前的文章没有针对于mvi做详细说明,才有了本篇对mvi的详细讲解,其实思路比较简单,就是使用Flow替换掉了livedata,为什么使用Flow呢,那只能说在kotln体系里面flow是强于rxjava了,本人喜欢使用代码作为实际说明,也有理解不足的地方,欢迎大家前来指正与交流,当前mvi已经基于之前的版本做了小升级,将原有mvi没有状态的设计现在加入到了现有的架构中,让架构更加灵活,同时flow貌似有些小改动在我之前使用其他人的例子出现api过时的情况,这里做出了调整,但未来可能当前使用的api还是会过时,这需要大家自行调整了。
举个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)
}
})
}
}
代码如下(示例):
/**
* 需要展示的状态,对应 UI 需要的数据
*/
interface IUiState
/**
* 来自用户和系统的是事件,也可以说是命令
*/
interface IUiEvent
/**
* 单次状态,即不是持久状态,类似于 EventBus ,例如加载错误提示出错、或者跳转到登录页,它们只执行一次,通常在 Compose 的副作用中使用
*/
interface IUiEffect
进行简化后的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) }
}
}
上面封装的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
}
代码如下(示例):
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("网络错误")
}
主要用于简化调用
代码如下(示例):
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)
}
}
代码如下(示例):
interface IHomeResponse {
/**
* 测试代码,用于演示,可以使用wanandroid api测试
*/
@GET("/xxx")
suspend fun getUserInfo(): ResultData<String>
}
代码如下(示例):
/**
* 状态管理
*/
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
}
代码如下(示例):
/**
* 处理请求
*/
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)
}
}
代码如下(示例):
/**
* 定义一个抽象类型子类需要实现
*/
abstract class FlowEvent
代码如下(示例):
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)
}
}
}
}
代码如下(示例):
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可以查看,链接一致就不重复提供了,本人经验有限可能有认知不足的地方欢迎指正。