android MVI到底是什么

前言

本篇文章的阅读对象是为了感觉好像了解MVI但是又不知道这玩意到底是个啥的读者
想理解MVI 需要提前理解几个东西
1.为什么推荐使用MVI,android 的MVI是基于什么提出的
2.android 的MVI是基于什么实现的,为什么要用这些

以上三点我先用最简短的语言以自己的理解先做一个解答

1,为什么推荐使用MVI,MVI是基于什么提出的

答:主要为了ViewModel层和View层的交互由双向转化为单向,并且规范交互数据传输

android端由mvc到mvp再到mvvm最后到mvi,每一次的变化都让代码分层更加清晰,目前MVVM的缺点是ViewModel和view的交互还是属于双向交互,viewModel和Model的处理界限也比较模糊,所以提出MVI,MVI其实是基于MVVM, 在View和ViewModel中增加了Intent来作为中间传输,通过响应编程更新UI实现的。这样不仅规范View与ViewModel交互,且将交互顺序由View—>ViewModel->View 的双向交互变为View->Intent->ViewModel->State->View的环形交互,通过Intent和State来解决ViewModel与Model的界限模糊问题。
也就是说ViewModel现在可以不关心如何被view触发,如何刷新UI,也不关心当前有多少数据模型,只用来维护Intent和state管理(再直白些就是intent就是view调用viewModel的中间层,state就是viewModel回调view的中间层,model通过intent和state去管理,看起来会更加简洁)

2,android 的MVI是基于什么实现的

目前android主流的MVI是基于协程+flow+viewModel去实现的
kotlin协程就不说了,省去接口回调,控制代码执行顺序,线程切换kotlin的协程功不可没
flow:中文翻译成流和Stream容易混淆,flow是响应式流,会有配备一个生产者和一个消费者(android可以理解成类似handler里的message,处理方式相似但是原理不同)
viewModel:jetpack家族,本来也可以自己写,但是jetpack提供了可以管理生命周期的viewModel不比自己写香么?

下面两个文章看看更加有助理解mvi

kotlin 响应式编程flow
https://juejin.cn/post/7034379406730592269
这篇文字几乎和官方文档写的详细程度差不多,但是解释会更加友好

MVVM使用
https://www.jianshu.com/p/f9d0688b241e
不喜欢看思路的可以通过这篇文章感受mvvm代码的层次结构

正片

这篇文章看完了能学会啥?
1.flow在UI中简单用法
2.Intent是个啥
3.state是个啥
4.原来MVI这么简单

1:flow在UI中简单用法

为啥我看MVI要先看flow?
因为没有flow就没有MVI的I的灵魂(如果你用rxjava或者自己创建监听者当我没说)
首先如果不知道flow怎么用的同学,我得说说你了,kotlin好好学学,mvvm都用kotlin写了,mvi还想着java是不是太过分了!(只针对android)

首先掏出官方例子

//所有的collect方法都是suspend修饰的,所以扔了协程里
runBlocking {
//创建一个流
     flow {

//用循环定义一个生产者
        for (i in 1..10) {
//生产者发10个数
            emit(i)
       }  
    }.collect {//注册这个流消费者
//消费者打印
           println(it)
   }
}

这个流很简单就是创建一个流,然后消费打印,用这段代码中两个方法比较重要,emit和collect,源码就不分析了就是emit是生产者发送数据,collect是消费者接受数据
然后我们把这个例子稍微复杂化一点放到例子里
ViewModel代码

class EnglishVM : ViewModel() {
    var flow=flow<Int> {
        for (i in 1..10) {
            emit(i)
        }
    }         
}

这是activity代码

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI学习")
        runBlocking {
            viewMode.flow.collect {
//将数字打印到textview上
                tvClass addText "$it"
            }
        }

    }
//做了个直接打印到textview的快捷方法,可以忽略
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

来看执行结果

执行结果

现在通过flow将文字展示到了UI上,但是有个问题,我们的业务场景一般是触发某个事件以后才会刷新UI,而且刷新UI我们只有一个或几个结果,不是一连串的数字,所以我们在这个基础上再次升级
首先flow这个方法已经不是那么好用了,我们引入一个新的概念StateFlow(我可以点)
StateFlow由两个API构成MutableStateFlow和StateFlow,主要用来通过状态类的变化来发送状态变化流。原理大体就是通过get,set去监听状态state变化,然后发送流,这里就不展开了,可以看各个不同版本的源码

然后将viewModel中的flow改为StateFlow并加入两个刷新UI的方法

class EnglishVM : BaseViewModel() {
//MutableStateFlow需要默认传入一个状态,我们随便传个1代表默认状态
   val state = MutableStateFlow<Int>(1)
//将状态改为2代表正在加载
    fun doLoading(){
        state.value = 2
    }
//将状态改为3代表加载完毕
    fun finishLoading(){
        state.value = 3
    }
}

然后给activity增加两个按钮,添加点击事件,分别调用doLoading和finishLoading

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI学习")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被点击"

            viewMode.finishLoading()
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被点击"
            viewMode.doLoading()
        }

         GlobalScope.launch  {
            viewMode.state.collect {
                tvClass addText "$it"
            }
        }

    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

运行并分别点击LOADING和FINISH


运行结果

好的一个简单的通过flow更新UI的效果已经完毕了,下面开始实现MVI

2:Intent是个啥

我可以很负责的告诉你,Intent就是个枚举,而且是个特殊的枚举,在kotlin中可以通过sealed关键字来生成封闭类,这个关键字生成的封闭类在when语句中可以不用谢else,而且由于是封闭类,所以可以通过数据对象来实现各种骚操作
比如下面的代码

//写个英语的意图
sealed class EngLishIntent {
//用数据类表示加载英语方法
    data class doLoadingEnglish(val num:Int):EngLishIntent()
//用匿名对象表示完成加载方法
    object finishLoading:EngLishIntent()
}

但是怎么用这个Intent呢?又涉及到一个kotlin的概念Channel(我可以点)
channel本来是用来做协程之间通讯的,而我们的view层的触发操作和viewModel层获取数据这个流程恰巧应该是需要完全分离的,并且channel具备flow的特性,所以用channel来做view和viewModel的通讯非常适合
我们通过再把上面的例子,通过Intent来处理下

意图代码如下

sealed class EngLishIntent {
//用数据类表示加载英语方法
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
//用匿名对象表示完成加载方法
    object FinishLoading:EngLishIntent()
}

viewModel将Intent引入

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
     val state = MutableStateFlow<Int>(1)
//初始化的时候将channel的消费者绑定
    init {
        handleIntent();
    }
//注册消费者
    private fun handleIntent() {
        viewModelScope.launch {
//将Channel转化为flow,并且注册消费者
            englishIntent.consumeAsFlow().collect {
//这里的it和Channel<EngLishIntent>泛型保持一致,所以it是封闭类(特殊枚举类)
                when(it){
//判断是FinishLoading 将state.value=3
                    is EngLishIntent.FinishLoading->{state.value=3}
//判断是DoLoadingEnglish 将state.value=1

                    is EngLishIntent.DoLoadingEnglish->{
                        //此处可以通过 it. 拿到DoLoadingEnglish的入参 后面会演示
                        state.value=2}
                }
            }
        }
    }

然后再把Activity改改

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI学习")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被点击"
//协程方法统一提取,方便日后修改
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
//拿到viewMode的englishIntent去传递意图
                viewMode.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被点击"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"
                viewMode.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }

        GlobalScope.launch {
            viewMode.state.collect {
                tvClass addText "$it"
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

然后看下点击两个按钮后的运行结果


运行结果

结果和上次的结果没什么太大的区别,而且感觉代码还变复杂了,为什么要这么做?
注意看下面两个图


原始方法

Intent

之前是直接使用viewModel提供的方法的,现在变成了传输intent里的枚举,彻底将View和ViewModel解耦了,现在唯一耦合的就是viewModel持有的Intent了,实现了业务解耦,很棒棒

既然知道了通过intent能实现view发起事件对viewModel的解耦,那能不能实现ViewModel刷新view的解耦呢?
其实上面的代码我们已经通过flow实现了一大半了,现在把int类型转换成一个枚举让代码更加严谨就能完全解耦了,此时就能引入MVI的最后一个概念state了

3:state是个啥

state是个和Intent一样的枚举,但是不同的是intent是个事件流,state是个状态流
首先我们先定义一个和Intent差不多的封装类state

sealed class EnglishState {
    object BeforeLoading:EnglishState()
    object Loading:EnglishState()
    object FinishLoading:EnglishState()
}

然后我们把之前的MutableStateFlow封装起来,不给view层修改权限,已保证我们业务逻辑不会写在UI层,并且把1、2、3等状态改为刚刚创建的EnglishState

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
    val state: StateFlow<EnglishState>
        get() = _state

    init {
        handleIntent();

    }

    private fun handleIntent() {
        viewModelScope.launch {
            englishIntent.consumeAsFlow().collect {
                when(it){
                   is EngLishIntent.FinishLoading->{
                        _state.value=EnglishState.FinishLoading
                    }
                    is EngLishIntent.DoLoadingEnglish->{
                        //此处可以通过 it. 拿到DoLoadingEnglish的入参 后面会演示
                        _state.value=EnglishState.Loading
                    }
                }
            }

        }
    }
}

然后把Activity的打印UI更新部分通过state做不同的逻辑处理

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI学习")
        btnFinishLoading.setOnClickListener {
            tvClass addText "btnFinishLoading 被点击"
            doLaunch{
                tvClass addText "send(EngLishIntent.FinishLoading)"
                viewModel.englishIntent.send(EngLishIntent.FinishLoading)
            }
        }
        btnLoading.setOnClickListener {
            tvClass addText "btnLoading 被点击"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"

                viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }

        lifecycleScope.launch {
            viewModel.state.collect {
                when(it){
                    is EnglishState.BeforeLoading->{
                        tvClass addText "初始化页面"

                    }
                    is EnglishState.Loading ->{
                        tvClass addText "加载中..."

                    }
                    is EnglishState.FinishLoading ->{
                        tvClass addText "加载完毕..."

                    }
                }
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) {
       this.text = "${this.text?.toString()}$text\n";
    }
}

分别点击按钮结果如下


image.png

到这里,一个基本的MVI就已经成型了,我们结合实际请求,稍稍做些许改动

4.原来MVI这么简单

我们先将ViewModel赋予真正的请求能力,提供一个基类(可以通过各种方法来)

open class BaseViewModel : ViewModel() {
    var getClient: () -> Urls = {
        val client = OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS) //设置超时时间
            .retryOnConnectionFailure(true)
        val logInterceptor = HttpLoggingInterceptor()
//        if (BuildConfig.DEBUG) {
//            //显示日志
//            logInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
//        } else {
//            logInterceptor.setLevel(HttpLoggingInterceptor.Level.NONE)
//        }
        client.addInterceptor(GsonInterceptor())
        Retrofit.Builder()
            .client(client.build())
            .baseUrl("https://route.showapi.com/")
            .addConverterFactory(ViewModelGsonConverterFactory())
            .build().create(Urls::class.java)
    }
//向协程提供一个全局异常,用来处理异常UI
    fun <T> errorContext(err: (errorMessage:Throwable) -> Unit):CoroutineExceptionHandler {
       return CoroutineExceptionHandler { _, e ->
           err.invoke(e)
       }
    }
}

intent 修改修改,加一个请求类型

sealed class EngLishIntent {
    //获取英语句子数据
    data class DoLoadingEnglish(val num:Int):EngLishIntent()
    //获取新闻数据
    object DoLoadingNews:EngLishIntent()
}

State也改改,新增几个数据状态

sealed class EnglishState {
    object BeforeLoading:EnglishState()
    object Loading:EnglishState()
    object FinishLoading:EnglishState()

    data class EnglishData(val list:List<EnglishKey>):EnglishState()
    data class NewsData(val list:List<NewsListKey>):EnglishState()

    data class ErrorData(val error:String):EnglishState();


}

viewmodel改改,带有真正的网络请求

class EnglishVM : BaseViewModel() {
    val englishIntent = Channel<EngLishIntent>(Channel.UNLIMITED)
    private val _state = MutableStateFlow<EnglishState>(EnglishState.BeforeLoading)
    val state: StateFlow<EnglishState>
        get() = _state
    init {
        handleIntent();
    }
    private fun handleIntent() {
        viewModelScope.launch {
            englishIntent.consumeAsFlow().collect {
                //这两种写法太冗余了
//                    is EngLishIntent.DoLoadingEnglish -> loadingEnglish()
//                    is EngLishIntent.DoLoadingNews -> loadingEnglish()
                commentLoading(it)
            }
        }
    }
   suspend fun intentToState(intent:EngLishIntent):EnglishState{
        when (intent) {
            //加载英语句子
            is EngLishIntent.DoLoadingEnglish ->
                return EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
            //加载新闻句子
            is EngLishIntent.DoLoadingNews ->
                return EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
        }
    }

    ////加载英语句子
//    private fun loadingEnglish() {
//        viewModelScope.launch(context = (errorContext {
//            _state.value = EnglishState.FinishLoading
//            _state.value = EnglishState.ErrorData(it.message?:"请求异常")
//        } + Dispatchers.Main)) {
//            _state.value = EnglishState.Loading
//            _state.value = EnglishState.EnglishData(getClient.invoke().getEnglishWordsByLaunch(5))
//            _state.value = EnglishState.FinishLoading
//        }
//    }
    //加载新闻
//    private fun loadingNews() {
//        viewModelScope.launch(context = (errorContext {
//            _state.value = EnglishState.FinishLoading
//            _state.value = EnglishState.ErrorData(it.message?:"请求异常")
//        } + Dispatchers.Main)) {
//            _state.value = EnglishState.Loading
//            _state.value = EnglishState.NewsData(getClient.invoke().getNewsListKeyByLaunch())
//            _state.value = EnglishState.FinishLoading
//        }
//    }
    private fun commentLoading(intent:EngLishIntent) {
        viewModelScope.launch(context = (errorContext {
            _state.value = EnglishState.FinishLoading
            _state.value = EnglishState.ErrorData(it.message?:"请求异常")
        } + Dispatchers.Main)) {
            _state.value = EnglishState.Loading
            _state.value = intentToState(intent)
            _state.value = EnglishState.FinishLoading
        }
    }
}

最后把activity的按钮改改,UI刷新逻辑改改变成这样

class MVIEnglishActivity :BaseActivity() {
    val viewModel :EnglishVM by viewModels<EnglishVM>()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent(R.layout.act_mvi_english_class)
        setTitle("MVI学习")
        btnLoadingNews.setOnClickListener {
            tvClass addText "btnLoadingNews 被点击"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingNews)"
                viewModel.englishIntent.send(EngLishIntent.DoLoadingNews)
            }
        }
        btnLoadingEnglish.setOnClickListener {
            tvClass addText "btnLoadingEnglish 被点击"
            doLaunch{
                tvClass addText "send(EngLishIntent.DoLoadingEnglish)"

                viewModel.englishIntent.send(EngLishIntent.DoLoadingEnglish(5))
            }
        }
//这里注意改成有生命周期的lifecycleScope 否则网络请求回来这里管道就销毁了
        lifecycleScope.launch {
            viewModel.state.collect {
                when(it){
                    is EnglishState.BeforeLoading->{
                        tvClass addText "初始化页面"

                    }
                    is EnglishState.Loading ->{
                        tvClass addText "加载中..."

                    }
                    is EnglishState.FinishLoading ->{
                        tvClass addText "加载完毕..."

                    }
                    is EnglishState.EnglishData->{
                        for (key in it.list){
                            tvClass addText key.english addText key.chinese

                        }

                    }
                    is EnglishState.NewsData->{
                        for (key in it.list){
                            tvClass addText "标题:${key.title}" addText "摘要:${key.summary}" addText "省份:${key.provinceName} 时间:${key.updateTime}"


                        }
                    }
                }
            }
        }

    }
    fun doLaunch(block: suspend CoroutineScope.() -> Unit){
        GlobalScope.launch {
            block.invoke(this)
        }
    }
    infix fun  TextView.addText(text: String) :TextView{
       this.text = "${this.text?.toString()}$text\n";
        return this
    }
}

最后附上接口

interface Urls {



    @GET("/1211-1")
   suspend fun getEnglishWordsByLaunch(
        @Query("count") count: Int?,
        @Query("showapi_appid") id: String = "测试id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<EnglishKey>

    @GET("/2217-4")
    suspend fun getNewsListKeyByLaunch(
        @Query("showapi_appid") id: String = "测试id",
        @Query("showapi_sign") showapi_sign: String = "showapi_sign",
    ): ArrayList<NewsListKey>

点击两次按钮后结果入下


image.png

一个简单的MVI网络请求架构到此结束

结尾

MVI其实主要思想是通过Intent将view和业务实现层分离,达到通过意图传递逻辑方法。所以不一定非要基于MVVM,也适用于MVP,这次分享就到此结束了
最后感谢
https://blog.csdn.net/vitaviva/article/details/109406873
这篇文章提供的清晰简单的思路,代码思路均由这篇文章获取

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容