对于一个应用,内部可以有多个架构模式并存,而不是一定要所有的页面都用MVVM或者MVP或者MVI,而应该根据业务需要,合适的选择适合的架构来做调整,甚至都可以不用这些架构。
首先来简单介绍下三种不同的模式:
MVC
MVC的整体架构如下:
M:Model 单独文件/package
V: View XML
C: Controller Activity/Fragment
优点:将Model进行了解耦
缺点: 1、静态:作为controller的actvity里面包含了视图的代码,他同时持有View和Model的应用,用来进行数据设置以及更新。 2、动态:随时需求的变化,由于acivity能访问view和model,越来越多的代码放到acivity,使得acivity变得越来越臃肿。
应用场景:设置/历史订单/交易记录/收藏,因为这些场景的逻辑简单。
MVP
MVP通过代码分层将Activity中的各项职能隔离开来,以接口回调的形式进行数据通信。 MVP的整体架构如下:
M: Model
V: View: xml + activity + view的interface
P: Presenter 逻辑 view和model的交互
优点:角色清晰
缺点:增加了interface->增加功能可能需要修改多个文件 应用场景:首页/复杂需求多变的页面
需要注意,不能让Presenter直接持有Activity,否则会导致Presenter中通过强转就能够获取到Activity的示例,进而使用Activity的功能,这导致了Presenter于Activity的耦合,这样又回到了MVC了,但是这个不是我们希望的。View可以直接调用Presenter。因此我们需要通过一个interface来Presenter与Activity的隔离。View不能直接和Model关联,需要通过Presenter层来实现交互。
示例代码:
持有的关系:Presenter持有了View的接口,同时Presenter层内部还持有了Model。通过这个接口,来实现试图的更新。测试源码链接:github.com/xingchaozha...
三、MVVM -> 双向数据绑定。相对于MVP,我们去掉了view的interface。现在有个问题,既然我们去掉了interface,但是我们的视图还是xml的,那怎么来响应数据的变化呢?谷歌于是采用了databinding来实现数据与视图的双向绑定。这就需要在xml中加入一些代码。就是databinding生成的xml
实现数据驱动是ViewModel,为什么叫ViewModel?因为其实这个就是View的数据。一般的,我们会使用LiveData,一种能够感知生命周期的数据,目前谷歌正在大力推广Compose,新的UI库,该方式与DataBinding已经逐渐的过时了。
M: Model
V: xml + activity
VM: ViewModel
优点:角色清晰
缺点:databinding ->xml包含了少量的代码,xml和activity进行了绑定, 如果要移除Activity,还需要对xml做处理。xml是activity的拖油瓶。 应用场景:首页/复杂需求多变的页面
测试代码:github.com/xingchaozha...
从MVC到MVP,我们让Activity的职责变得单一,从MVP到MVVM,我们干掉了view的interface,但是还有xml,于是我们想,能不能把xml也干掉?让Activity只专注于视图展示?
使用Compose的方式就可以完全规避掉这些问题了。
Compose
天生就是数据驱动 ,不需要使用viewbinding与DataBinding,导致fragment/livedata/databinding/viewbinding/view/viewgroup的过时。
M: Model
V: activity + composable
VM: ViewModel, 数据类型变了 livedata ->state的数据,如果数据想要自动更新,就需要使用MutableState 为什么说Compose自带数据驱动?因为Compose采用了声明式UI,传统的View采用了命令式布局,需要动态的为每一个View手动设置值。
使用下边的声明式的View来确定View的状态:
ini
fun MyMainView(viewModel:MyViewModel) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Companion.CenterHorizontally
) {
LazyVerticalGrid(
cells = GridCells.Fixed(3)
) {
items(9) {
TextButton(
enabled = true,
onClick = { viewModel.onClickedCellAt(it / 3, it % 3) },
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
.height(88.dp)
.background(
Color.Gray
)
.testTag(it.toString())
) {
Text(viewModel.cellList[it].value ?: "", fontSize = 25.sp, color = Color.White)
}
}
}
Text(text = viewModel.winner.value, modifier = Modifier.padding(10.dp), fontSize = 25.sp)
if (viewModel.winner.value.isNotEmpty()) {
Text(text = "赢了", fontSize = 25.sp)
}
}
}
Compose的ViewModel:
必须要使用MutableState,才能正常的关注数据状态的变化。
kotlin
class MyViewModel : ViewModel() {
private val model: Board = Board()
val winner = mutableStateOf("")
val cellList: List<MutableState<String?>> = ArrayList()
fun onResetSelected() {
model.restart()
for (i in 0..8) {
cellList[i].value = ""
}
winner.value = ""
}
fun onClickedCellAt(row: Int, col: Int) {
val playerThatMoved = model.mark(row, col)
if (playerThatMoved != null) {
winner.value = (if (model.winner == null) "" else model.winner.toString())
cellList[row * 3 + col].value =
if (playerThatMoved == null) null else playerThatMoved.toString()
}
}
init {
for (i in 0..8) {
(cellList as ArrayList).add(mutableStateOf(""))
}
}
}
MutableState
是一种特殊的状态容器,用于持有UI状态的值。当这个值改变时,MutableState
确保使用该状态的可组合(@Composable)函数被重新调用,从而更新UI。这种机制是Compose响应式编程模型的核心,它允许UI自动响应状态的变化。
Activity中只需要简单的调用即可:
kotlin
class MyActivity : AppCompatActivity() {
@OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModelV2 = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
setContent {
MyMainView(viewModelV2)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_jingziqi, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_reset -> {
val viewModel: MyViewModel? by viewModels()
viewModel!!.onResetSelected()
true
}
else -> super.onOptionsItemSelected(item)
}
}
}
测试代码:github.com/xingchaozha...
附加:MVI的简单介绍
使用Compose之后,MVI的使用场景变得更小了。 MVI模式的核心思想是将应用的状态管理和UI渲染过程形式化,通过一系列的严格定义的状态变换来响应用户操作,从而使应用更易于理解和维护。
工作流程
- 用户交互:用户与UI交互,产生Intent。
- 处理Intent:系统根据Intent处理业务逻辑,并更新Model。
- 更新Model:每次Model改变都会生成一个新的Model实例。
- 反应Model:View观察Model的变化,并根据最新的Model更新UI。
在MVI模式中,所有事情都是一系列明确的步骤:用户行为(Intent)导致应用状态(Model)的更新,而新的应用状态又通过UI(View)展示给用户,形成一个闭环。通过这种方式,MVI使应用的状态变化和数据流变得清晰和可预测,从而降低了应用的复杂性,提高了可维护性。
- 所有的操作最终都会转换成
State
,所以当复杂页面的State
容易膨胀 state
是不变的,因此每当state
需要更新时都要创建新对象替代老对象,这会带来一定内存开销
一些问题
1、为什么compose中需要使用MutableState,而MVVM中的ViewModel需要使用LiveData?
-
MutableState是Compose专门为状态管理设计的一种原语。它是一个容器,持有UI状态的当前值,并且当这个值发生变化时,能够通知Compose系统重新渲染依赖于这个状态的UI部分。这种机制非常适合Compose的声明式和响应式UI更新模式。
-
使用MutableState不仅使状态的更新和UI的重渲染变得高效,而且还紧密集成了Compose的重组机制,确保了轻量级的状态管理并减少了不必要的UI更新。
-
LiveData是一个可观察的数据持有者类,专门设计用于以生命周期感知的方式
命令式编程在ViewModel和UI组件之间共享和管理UI相关数据。当LiveData中的数据变化时,观察这些LiveData的UI组件会收到通知,并据此更新界面。
- LiveData与ViewModel一起使用,支持数据绑定和生命周期感知,减少了内存泄漏的风险和处理生命周期变化的复杂性。它适用于传统的命令式UI更新机制,确保数据变化能够安全、正确地反映到UI上。
2、声明式与命令式编程的差别
命令式编程关注"如何做",步骤明确 声明式编程关注"要做什么",隐藏实现细节
命令式,如果想根据某个条件改变这个按钮的文本,需要写一个判断逻辑:
js
// 你需要通过调用例如setXXX等类似的方法来实现View状态的更新,这个就是命令。
if (someCondition) {
myButton.setText("Condition Met");
} else {
myButton.setText("Condition Not Met");
}
如果采用声明式:
js
@Composable
fun MyButton(showButton: Boolean, condition: Boolean) {
if (showButton) {
Button(onClick = { /* Do something */ }) {
// 你不需要通过命令来实现更新,而只是告诉View,你在这个状态下应该显示成什么样的,这就是声明。
Text(if (condition) "Condition Met" else "Condition Not Met")
}
}
}