kotlin安卓Android细碎技术知识点

android MVI到底是什么

2022-02-10  本文已影响0人  bridegg

前言

本篇文章的阅读对象是为了感觉好像了解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
这篇文章提供的清晰简单的思路,代码思路均由这篇文章获取

上一篇下一篇

猜你喜欢

热点阅读