学更好的别人,
做更好的自己。
——《微卡智享》
本文长度为3786字,预计阅读8分钟
前言
做Android开发的应该都听到过Android的架构,什么MVC,MVP,MVVM,所有的架构来说也没有什么完美之说。当时在还记得最初刚接触Android时,是因为要做一个PDA的盘点机,也是因为有目标和方向,所以从头开始自学的Android并完成了这个程序,当时的目的是完成,所以根本就谈不上什么架构,但也因为这个算是入门了Android。后面接触的久了后也开始慢慢了解架构,而我算是没经历过MVP的架构,出了Kotlin后就直接开始学习使用的,也是后面的项目中直接MVVM开整了。
MVI与MVVM非常接近,可以更针对性地解决一些MVVM中解决不了的问题。Android的应用架构指南,发现谷歌推荐的最佳实践已经变成了单向数据流动 + 状态集中管理,也正是说明MVI架构相比MVVM的核心点。
什么是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进行数据请求
如下图:
从上图中可以看到,MVI最核心的还是单向数据流动,用户用Intent的形式通知Model,Model基于Intent的通知更新State,View通过State接收到的变化来更新UI界面。
MVI架构Demo
微卡智享
微卡智享
MVI运行效果
微卡智享
程序目录
上图是做的一个MVI的架构的小Demo,整体的流程图大概如下:
01
项目Gradle引用
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
接收的ActionIntent使用了Channel,在《Android Kotlin协程间的通信Channel介绍》介绍过,这里使用的是Channel.UNLIMITED,可以保证Send的不挂起,使用consumeAsFlow,可以将Channel转换为ChannelFlow热流进行处理。
这里使用consumeAsFlow而不是receiveAsFlow,主要是我们在这里只有一个接收器,关于consumeAsFlow和receiveAsFlow的区别,就是使用 consumeAsFlow() 只能有一个消费者。使用 receiveAsFlow() 可以有多个消费者,但当向 Channel 中发射一个数据之后,收到该元素的消费者是不确定的。
处理State
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
点击按钮发送Intent
设置ViewModel的State的观察者
这样一个MVI架构的Demo就完成了。
微卡智享
MVI的总结
MVI架构的优点:
数据单向流动,可以更简单地对状态变化进行跟踪和回溯
使用ViewState对State集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码
ViewModel通过ViewState与Action通信,通过浏览ViewState 和 Aciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。
当然MVI架构也有其缺点:
所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销。
所有的架构都有其优缺点,并不是完美的,在开发中还是需要自己来使用最适合的。
微卡智享
Demo源码
https://github.com/Vaccae/AndroidMVIDemo.git
点击阅读原文可以看到“码云”的地址
完
往期精彩回顾
Android Kotlin协程间的通信Channel介绍
Android内存篇(三)----自动重启APP实现内存兜底策略
Android内存篇(二)---JVMTI在Anroid8.1下的使用