• Mvi架构浅析


    • 本篇文章是简单使用了Kotlin + 协程 + flow + channel写了一个伪登录请求案例(dev_20220804_mvi分支),通过该案例的来了解Mvi架构。
    • 在了解Mvi之前,建议先了解一下Mvvm,可以参考Mvc、Mvp和Mvvm

    一.代码环节

    • 单单先去了解概念会有一种抽象的感觉,我们通过分析代码的逻辑以及代码对应的类结合Mvi的概念一同理解,会清晰很多;
    • 案例一共包含4个类,MainActivity、DemoViewModel、DemoIntent、DemoUiState,先贴上;

    1.1.MainActivity

    import android.os.Bundle
    import android.view.View
    import android.widget.TextView
    import androidx.activity.viewModels
    import androidx.appcompat.app.AppCompatActivity
    import androidx.lifecycle.lifecycleScope
    import kotlinx.coroutines.launch
    
    class MainActivity : AppCompatActivity() {
    
        private lateinit var textView: TextView
    
        private val mDemoViewModel: DemoViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            textView = findViewById(R.id.textView)
    
            lifecycleScope.launch {
                //3.View层设置监听(VM层根据意图执行相应的逻辑后会主动触发该回调)
                mDemoViewModel.mUiState.collect { uiState ->
                    when (uiState) {
                        is DemoUiState.loginSuccess -> {
                            textView.setText(uiState.success)
                        }
    
                        is DemoUiState.loginFail -> {
                            textView.setText("登录失败")
                        }
    
                        is DemoUiState.beforeLogin -> {
                            textView.setText(uiState.bengin)
                        }
                    }
                }
            }
        }
    
        //1.点击按钮模拟网络请求
        fun login(view: View) {
            lifecycleScope.launch {
                //1.1.通过管道将意图传递给ViewModel层
                mDemoViewModel.mChannel.send(DemoIntent.LoginIntent)
            }
        }
    
        override fun onDestroy() {
            super.onDestroy()
            //Channel是一种协程资源['热'流],跨越不同的协程进行通信
            //在使用完若不去关闭,会造成不必要的浪费
            mDemoViewModel.mChannel.close()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    1.2.DemoViewModel

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.channels.Channel
    import kotlinx.coroutines.channels.consumeEach
    import kotlinx.coroutines.flow.*
    import kotlinx.coroutines.launch
    
    /**
     * 意图类
     * 可以将意图类写在对应的ViewModel类中,不需要分开出去
     *      好处:避免粒度分的过细,从而增加项目的复杂度
     */
    sealed class DemoIntent {
        //意图的个数根据实际的需求进行添加
        object LoginIntent : DemoIntent()
    }
    
    class DemoViewModel : ViewModel() {
    
        val mChannel = Channel<DemoIntent>()
    
        private val mDemoUiState = MutableStateFlow<DemoUiState>(DemoUiState.beforeLogin("准备登录"))
        //使用flow来监听
        val mUiState: StateFlow<DemoUiState> = mDemoUiState
    
        init {
            handleIntentInfo()
        }
    
        private fun handleIntentInfo() {
            viewModelScope.launch {
                mChannel.consumeEach{//consumeEach:channel(管道)读数据推荐方案[直接使用receive是很容易出问题的]
                    //2.接收用户传输过来的意图
                    when(it){
                        //这里为什么要经过一个意图呢?而不是有通信即可,目的是为了便于管理,视图是为了V层跟VM层更好地解耦 以及代码扩展
                        is DemoIntent.LoginIntent -> getLoginData()
                    }
                }
            }
        }
    
        private fun getLoginData() {
            viewModelScope.launch {
                requestLogin.flowOn(Dispatchers.Default)
                    .catch { exception ->
                        mDemoUiState.value = DemoUiState.loginFail(exception)
                    }.collect { loginInfo ->
                        mDemoUiState.value = DemoUiState.loginSuccess(loginInfo)
                    }
            }
        }
    
        private val requestLogin: Flow<String> = flow {
            val loginInfo = "登录成功"       //模拟请求登录接口
            emit(loginInfo)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    1.3.DemoUiState

    sealed class DemoUiState {
        //为了降低复杂度,方法的参数都写定(实际项目中括号内部的参数建议封装成泛型)
        data class beforeLogin(var bengin: String):DemoUiState()
        data class loginSuccess(var success: String):DemoUiState()
        data class loginFail(var exception: Throwable):DemoUiState()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 接下来我们结合概念和代码来理解Mvi

    二.Mvi

    2.1.站在Android开发者的角度简单理解Mvi

    • 假如用户在操作我们的App,要执行登录操作,那么代码层面会将用户的登录操作封装层一个意图(对应Mvi的i),然后将这个意图传递给ViewModel(M层的一部分),然后ViewModel执行完相应的逻辑后,会通过回调的方式通知View层(实现方式有多种,常用的就是Livedata,案例中用的是Flow);
    • 上面的场景,用户的操作从V层开始,最终的反馈也是在View层,数据的流向是单向的;

    2.2.Mvi和Mvvm的区别

    • 多了一个i(对应案例的DemoIntent 类)和少了一个databinding,其它的个人觉得没什么区别(至于DemoUiState 类,也可以在Mvvm中也有对应的封装);

    2.3.对比一下代码逻辑

    • 从MainActivity的login方法开始,用户执行登录,会通过协程的管道(协程的一种资源,不是本文的重点,了解即可)传递一个DemoIntent.LoginIntent参数,其中DemoIntent类就是一个意图类,LoginIntent是一个具体的意图,至于需要在DemoIntent中定义多少个意图由具体的需求来决定。将登录的需要包装成一个意图然后传递到ViewModel层,ViewModel层接收了,对应在DemoViewModel类的handleIntentInfo方法中处理View层传递过来的意图,我们只要分析该方法的When语句即可,判断it到底对应哪个DemoIntent的意图来决定执行逻辑,当是DemoIntent.LoginIntent执行登录的(伪)网络请求()调用到getLoginData方法,请求接口后修改mDemoUiState.value的值,从而触发MainActivity类中的mDemoViewModel.mUiState设置的监听,然后View层根据不同的条件执行不同的逻辑;

    三.总结

    • 本篇文章从具体的代码描述Mvi不同于Mvvm的点,Mvi的M层可以对应Mvvm的VM层和M层的结合,同时包含意图和UI状态改变的两个部分。使用层面更简单了,但是个人觉得Mvi的M层于Mvvm的臃肿了一些,而这个臃肿的点大家可以学习Mvvm的优点对Mvi进行灵活变动。
    • 关于Mvi的学习,个人推荐文章-MVVM 进阶版:MVI 架构了解一下~
  • 相关阅读:
    00_C语言学习笔记
    记录一下工作中completableFutures异步多线程的使用
    进程的状态
    【TA】OP-TEE demo学习
    LeetCode 1553 吃掉N个橘子的最少天数
    特约|数码转型思考:Web3.0与银行
    Object.defineProperty也能监听数组变化?
    SQL干货丨关于分组和聚合函数,如何实现查询排名?!
    Vue 生命周期钩子解读
    [Linux/初学者]Vim文本编译器的模式切换及其常用指令
  • 原文地址:https://blog.csdn.net/itTalmud/article/details/126166742