Android MVI架构初探

学更好的别人,

做更好的自己。

——《微卡智享》

Android MVI架构初探_第1张图片

本文长度为3786,预计阅读8分钟

前言

做Android开发的应该都听到过Android的架构,什么MVC,MVP,MVVM,所有的架构来说也没有什么完美之说。当时在还记得最初刚接触Android时,是因为要做一个PDA的盘点机,也是因为有目标和方向,所以从头开始自学的Android并完成了这个程序,当时的目的是完成,所以根本就谈不上什么架构,但也因为这个算是入门了Android。后面接触的久了后也开始慢慢了解架构,而我算是没经历过MVP的架构,出了Kotlin后就直接开始学习使用的,也是后面的项目中直接MVVM开整了。

MVI与MVVM非常接近,可以更针对性地解决一些MVVM中解决不了的问题。Android的应用架构指南,发现谷歌推荐的最佳实践已经变成了单向数据流动 + 状态集中管理,也正是说明MVI架构相比MVVM的核心点。

ad968b5441aaec2c14468bab090d0f8f.png

什么是MVI架构?

A

MVI即Model-View-Intent,

  • Model:与其他MVVM中的Model不同的是,MVI的Model主要指UI状态(State)。当前界面展示的内容无非就是UI状态的一个快照:例如数据加载过程、控件位置等都是一种UI状态

  • View:与其他MVX中的View一致,可能是一个Activity、Fragment或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(不是Activity的Intent、后面介绍)

  • Intent:此Intent不是Activity的Intent,用户的任何操作都被包装成Intent后发送给Model进行数据请求

如下图:

Android MVI架构初探_第2张图片

从上图中可以看到,MVI最核心的还是单向数据流动,用户用Intent的形式通知Model,Model基于Intent的通知更新State,View通过State接收到的变化来更新UI界面。

MVI架构Demo

8b4a723b886e94b323115a13c256e7eb.png

微卡智享

微卡智享

MVI运行效果

‍‍

微卡智享

程序目录

Android MVI架构初探_第3张图片

上图是做的一个MVI的架构的小Demo,整体的流程图大概如下:

Android MVI架构初探_第4张图片

01

项目Gradle引用

Android MVI架构初探_第5张图片

implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'


    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'


    //使用协程
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"

界面列表的展示用了BaseRecyclerViewAdapter,这个在我以前文章《Android BaseQuickAdapter3.0.4版本二级列表的使用及遇到的问题》,然而后就是协程coroutines和lifecycle。

02

设置用户Intent和更新UI时的State

ActionIntent

sealed class ActionIntent {


    //加载药品列表
    object LoadDrugs : ActionIntent()


    //添加药品信息
    object InsDrugs : ActionIntent()


    //删除药品信息
    data class DelDrugs(val idx: Int, val item: CDrugs) : ActionIntent()
}

Intent中加入三个事件,分别的加载药品列表,添加药品信息和删除药品信息。

ActionState

sealed class ActionState {


    object Normal : ActionState()
    object Loading : ActionState()


    data class Drugs(val drugs: MutableList) : ActionState()


    data class Error(val msg: String?) : ActionState()
}

State中设了四个状态,Normal是常规状态,Loading是点击按钮时的加载状态,Drugs是更新药品信息列表,Error是错误信息状态。

03

ViewModel中设置接收Intent和更新State

class MainViewModel : ViewModel() {


    private val _respository = DrugsRepository()


    val actionIntent = Channel(Channel.UNLIMITED)
    private val _actionstate = MutableSharedFlow()
    val state: SharedFlow
        get() = _actionstate


    var listDrugs = mutableListOf()


    init {
        initActionIntent()
        _actionstate.tryEmit(ActionState.Normal)
    }


    private fun initActionIntent() {
        viewModelScope.launch {
            actionIntent.consumeAsFlow().collect {
                when (it) {
                    is ActionIntent.LoadDrugs -> LoadDrugs()
                    is ActionIntent.InsDrugs -> InsDrugs()
                    is ActionIntent.DelDrugs -> {
                        DelDrugs(it.idx)
                    }
                }
            }
        }
    }


    private fun DelDrugs(idx: Int) {
        viewModelScope.launch {
            if (idx < 0) {
                _actionstate.emit(ActionState.Error("未选中要删除的药品信息"))
                return@launch
            }
            //修改为加载状态
            _actionstate.emit(ActionState.Loading)
            //开始加载数据
            _actionstate.emit(
                try {
                    listDrugs.removeAt(idx)
                    ActionState.Drugs(listDrugs)
                } catch (e: Exception) {
                    ActionState.Error(e.message.toString())
                }
            )
            //恢复状态
            _actionstate.emit(ActionState.Normal)
        }
    }


    private fun InsDrugs() {
        viewModelScope.launch {
            //修改为加载状态
            _actionstate.emit(ActionState.Loading)
            //开始加载数据
            _actionstate.emit(
                try {
                    listDrugs.add(_respository.getNewDrugs())
                    ActionState.Drugs(listDrugs)
                } catch (e: Exception) {
                    ActionState.Error(e.message.toString())
                }
            )
            //恢复状态
            _actionstate.emit(ActionState.Normal)
        }
    }


    //加载药品信息
    private fun LoadDrugs() {
        viewModelScope.launch {
            //修改为读取状态
            _actionstate.emit(ActionState.Loading)
            //开始加载数据
            _actionstate.emit(
                try {
                    listDrugs = _respository.createDrugs()
                    ActionState.Drugs(listDrugs)
                } catch (e: Exception) {
                    ActionState.Error(e.message.toString())
                }
            )
            //恢复状态
            _actionstate.emit(ActionState.Normal)
        }
    }
}

处理Intent

554a0924196f4d6da5ea8758d646a1f7.png

Android MVI架构初探_第6张图片

接收的ActionIntent使用了Channel,在《Android Kotlin协程间的通信Channel介绍》介绍过,这里使用的是Channel.UNLIMITED,可以保证Send的不挂起,使用consumeAsFlow,可以将Channel转换为ChannelFlow热流进行处理。

这里使用consumeAsFlow而不是receiveAsFlow,主要是我们在这里只有一个接收器,关于consumeAsFlow和receiveAsFlow的区别,就是使用 consumeAsFlow() 只能有一个消费者。使用 receiveAsFlow() 可以有多个消费者,但当向 Channel 中发射一个数据之后,收到该元素的消费者是不确定的。

处理State

Android MVI架构初探_第7张图片

Android MVI架构初探_第8张图片

State的状态这里采用的是ShareFlow,而不是StateFlow,从上图中处理删除药品信这个函数中可以看到,处理这个时首先改为加载状态(UI界面中会让按钮变成DisEnable,防止重复点击),然后进行数据处理,当完成后再改为Normal的状态。如果这里改为StateFlow,在极短的时间内同时修改StateFlow值,UI界面的观察者只能接收到最后的更新值,也就是说数据没更新,只接收到了ActionState.Normal的状态。

04

MainActivity设置

class MainActivity : AppCompatActivity() {


    private val recyclerView: RecyclerView by lazy { findViewById(R.id.recycler_view) }
    private val btncreate: Button by lazy { findViewById(R.id.btncreate) }
    private val btnadd: Button by lazy { findViewById(R.id.btnadd) }
    private val btndel: Button by lazy { findViewById(R.id.btndel) }


    private lateinit var mainViewModel: MainViewModel
    private lateinit var drugsAdapter: DrugsAdapter


    //adapter的位置
    private var adapterpos = -1


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)


        drugsAdapter = DrugsAdapter(R.layout.rcl_item, mainViewModel.listDrugs)
        drugsAdapter.setOnItemClickListener { baseQuickAdapter, view, i ->
            adapterpos = i
        }


        val gridLayoutManager = GridLayoutManager(this, 3)
        recyclerView.layoutManager = gridLayoutManager
        recyclerView.adapter = drugsAdapter


        //初始化ViewModel监听
        observeViewModel()


        btncreate.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.actionIntent.send(ActionIntent.LoadDrugs)
            }
        }


        btnadd.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.actionIntent.send(ActionIntent.InsDrugs)
            }
        }


        btndel.setOnClickListener {
            lifecycleScope.launch {
                Log.i("status", "$adapterpos")
                val item = try {
                    drugsAdapter.getItem(adapterpos)
                } catch (e: Exception) {
                    CDrugs()
                }
                mainViewModel.actionIntent.send(ActionIntent.DelDrugs(adapterpos, item))
            }
        }
    }


    private fun observeViewModel() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.state.collect {
                    when (it) {
                        is ActionState.Normal -> {
                            btncreate.isEnabled = true
                            btnadd.isEnabled = true
                            btndel.isEnabled = true
                            Log.i(
                                "status",
                                "normal create:${btncreate.isEnabled}, add:${btnadd.isEnabled}, del:${btndel.isEnabled}"
                            )
                        }
                        is ActionState.Loading -> {
                            btncreate.isEnabled = false
                            btncreate.isEnabled = false
                            btncreate.isEnabled = false
                            Log.i(
                                "status",
                                "loading create:${btncreate.isEnabled}, add:${btnadd.isEnabled}, del:${btndel.isEnabled}"
                            )
                        }
                        is ActionState.Drugs -> {
                            Log.i("status", "drugsqty:${it.drugs.size}")
                            drugsAdapter.setList(it.drugs)
//                            drugsAdapter.setNewInstance(it.drugs)
                        }
                        is ActionState.Error -> {
                            Toast.makeText(this@MainActivity, it.msg, Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }
}

加载VIewModel

Android MVI架构初探_第9张图片

点击按钮发送Intent

Android MVI架构初探_第10张图片

设置ViewModel的State的观察者

Android MVI架构初探_第11张图片

这样一个MVI架构的Demo就完成了。

微卡智享

MVI的总结

MVI架构的优点:

  1. 数据单向流动,可以更简单地对状态变化进行跟踪和回溯

  2. 使用ViewState对State集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewState与Action通信,通过浏览ViewState 和 Aciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。

当然MVI架构也有其缺点:

  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销。

所有的架构都有其优缺点,并不是完美的,在开发中还是需要自己来使用最适合的。

微卡智享

Demo源码

https://github.com/Vaccae/AndroidMVIDemo.git

点击阅读原文可以看到“码云”的地址

cd3ed688bf0d3a72e8797cdc870c7670.png

856d11500d9fb19f48d9e92a9dc758c9.png

往期精彩回顾

Android MVI架构初探_第12张图片

Android Kotlin协程间的通信Channel介绍


Android MVI架构初探_第13张图片

Android内存篇(三)----自动重启APP实现内存兜底策略


Android MVI架构初探_第14张图片

Android内存篇(二)---JVMTI在Anroid8.1下的使用


你可能感兴趣的:(java,python,android,安卓,数据库)