Android中页面架构MVC、MVP、MVVM、Compose的应用与区别

对于一个应用,内部可以有多个架构模式并存,而不是一定要所有的页面都用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渲染过程形式化,通过一系列的严格定义的状态变换来响应用户操作,从而使应用更易于理解和维护。

工作流程

  1. 用户交互:用户与UI交互,产生Intent。
  2. 处理Intent:系统根据Intent处理业务逻辑,并更新Model。
  3. 更新Model:每次Model改变都会生成一个新的Model实例。
  4. 反应Model:View观察Model的变化,并根据最新的Model更新UI。

在MVI模式中,所有事情都是一系列明确的步骤:用户行为(Intent)导致应用状态(Model)的更新,而新的应用状态又通过UI(View)展示给用户,形成一个闭环。通过这种方式,MVI使应用的状态变化和数据流变得清晰和可预测,从而降低了应用的复杂性,提高了可维护性。

  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
  2. 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")
        }
    }
}
相关推荐
黑客老陈1 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安1 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
暴富的Tdy1 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端
z千鑫2 小时前
【前端】入门指南:Vue中使用Node.js进行数据库CRUD操作的详细步骤
前端·vue.js·node.js
m0_748250743 小时前
Web入门常用标签、属性、属性值
前端
m0_748230443 小时前
SSE(Server-Sent Events)返回n ,前端接收数据时被错误的截断【如何避免SSE消息中的换行符或回车符被解释为事件消息的结束】
前端