Android mvi 第二篇

文章目录

  • 前言
  • 一、插个题外话,如何将java异步回调编程协程异步
  • 二、说到mvi的核心,那就是view_model和flow拓展下面一一讲解
    • 1. 定义通用接口,接口包含了‘状态’、‘事件’、‘单次加载状态’
    • 2. 封装ViewModel
    • 3.封装LoadViewModel
      • * 封装通用的数据处理类
      • * 定义通用加载事件
      • * 封装LoadViewModel
    • 4. 定义拓展
    • 5. 使用
      • * 定义Response
      • * 定义Contract
      • * 定义ViewModel
      • * 具体调用
  • 三. 提供一个使用flow定义的轻量级eventbus
    • 1. 定义数据类
    • 2. 封装类
    • 3. 使用
  • 总结


前言

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

鉴于之前的文章没有针对于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可以查看,链接一致就不重复提供了,本人经验有限可能有认知不足的地方欢迎指正。

你可能感兴趣的:(android,kotlin,mvi,flow)