如何应对Android面试官 -> 玩转 MVx(MVC、MVP、MVVM、MVI)

前言


image.png

本章主要基于以下几个方向进行 MVx 的讲解,带你玩转 MVx;

MVC、MVP、MVVM、MVI 它们到底是什么?


分文件、分模块、分模式

一个文件打天下

为什么不要用一个页面打天下?

页面是给用户看的,随着版本的迭代,页面上的交互需求也在不断的变化,导致页面不断的在修改,页面在不断的修改,那么这个文件中就不能放太多的代码,否则导致后续迭代起来就很困难;

页面很复杂怎么办?

一个复杂页面,也是由一个一个的 View 组成的,那么我们就可以将 View 拆成自定义 View。同时将这个 View 的相关逻辑,拆分到对应的逻辑处理层;

应用开发原则

  • 遵循面向对象的SOLID原则;
  • 视图、数据、逻辑分离;

六大原则

image.png

  • 单一职责

一个 class 完成一件事情;一个 class 只做一件事情,当有更多事情的时候,使用继承,那么就引申出了『开闭原则』;

  • 开闭原则

对继承开放,对修改关闭(不能改变基类中的逻辑);多个类文件完成多件事情的时候,使用继承,对文件尽量不做修改;

  • 里氏替换原则

子类覆写父类函数的时候,不能改变父类的逻辑;例如:「基类车 有一个 run 方法,子类覆写之后不能改成 fly 方法」

  • 依赖倒置原则

不依赖实现,只依赖接口;「例如车在路上跑,车不应该依赖路的具体实现,而是一个路的接口」;

  • 接口隔离原则

接口的粒度要小,接口的最小化;『例如,人和路的接口要分别定义,而不是定义成一个接口』;

  • 迪米特原则

最小支持原则;

视图、数据、逻辑分离

从静态角度的一个分离,以及生命周期的控制,数据需要在什么情况下进行销毁;

App 架构设计

image.png

整体遵循一个 层次化、模块化、控件化;

而模块化又可以细分成:组件化、插件化;

MacHi 2024-06-05 20-48-18.png
image.png
组件化一般针对的都是业务组件,业务组件之间互相不依赖,公共部分抽象成接口,下沉到通用组件;组件化的意义是通过 App 模块把这些组件拼装起来,组装成一个 APK,这种就是在编译期间,它们是一个一个的组件;

而插件化,本质上可以理解为,把这一个一个的组件打包成一个一个的无图标的 APK,然后通过后下载的方式集成到应用中,以此来缩小安装包的体积;

MVx

在 MVx 中,这个 M 基本都是一样的,V 的角色其实是有一些差异的;

MVC

数据层、视图层、控制层(逻辑层)如何定义?

image.png

  • 数据层

对数据 + 对数据进行的操作(不依赖视图的操作);例如我们利用 RxJava 对数据进行变换的操作,map、flatMap等等,放到 model 层执行;

  • 视图层

不同的模式有不同的定义,在 Android 中就是 Activity + xml + fragment;

  • 控制层

view 和 model 之间的通信和交互;

在 Andriod 中 xml 布局的功能性太弱, Activity 实际上负责了 View 层与 Controller 层两者的工作,所以在 Android 中 MVC 更像是这种形式,而 Model 层好多人直接把 bean 写在了 model 下,它俩并不能完全相等;

image.png

MVC 相对一个文件打天下

优点是:抽离了 model;

缺点是:controller 的权力太大,在 Android 中 Activity 承担了 Controller 的角色,如果这个 Activity 页面逻辑太多的话,这个 Activity 依然会变得特别臃肿,就又回到了一个文件打天下的场景了;

那么,怎么办呢?变! MVP 来了

MVP

image.png

  • Model

主要负责数据的处理,这个没有什么变化;

  • View

对应于 Activity 与 XML,只负责显示 UI,只与 Presenter 层交互,与 Model 层没有耦合;

  • Presenter

主要负责处理业务逻辑,通过接口回调 View 层;

MVP 相对 MVC 的

优点是:在 MVC 的基础上通过 Interface 彻底分离了 View 和 Model,Activity 只剩下了 View,Presenter 承担了 View 和 Model 之间的交互,满足了单一职责,对视图数据的逻辑分离是清晰的

缺点是:引入了 interface,方法增多,引入一个方法要改很多地方,并且要面临着回调地狱;

那怎么办呢?变,MVVM 来了;

MVVM

引入 MVVM 的模式,本质上就是在 MVP 的基础上的一次变革,就是要把引入的 interface 给干掉!

image.png

可以看出 MVVM 与 MVP 最大的区别是,不用主动去刷新 UI 了,只要 model 层数据变了,就会自动更新到 UI 上;

那么在 Android 上实现数据的双向绑定是通过 DataBinding 实现的,通过 DataBinding 就是要把数据自动刷新到 View 上,View 上面有什么事件,要绑定到 ViewModel 上来;

不过,很多开发者并不喜欢使用 DataBinding,因为它需要你在 xml 中写一些逻辑,并且要在 gradle 中引入 DataBinding 的支持,就会导致文件路径的更改,DataBinding 不能自动更新等一些 ide 上比较难用的问题;

引入 DataBinding 很简单,只需要在使用它的 module 中加入下面这个代码就行:

dataBinding {enable true
}

这里额外插入一个 viewBinding,viewBinding 和 DataBinding 还是有区别的,viewBinding 只能省略 findViewById 的操作;DataBinding 除了 viewBinding 的能力之外,还能绑定数据,同时需要修改 xml,而 viewBinding 不需要修改 xml;

Android 上的 MVVM 一定要结合 DataBinding 来实现,否则就会变成下面的这种

image.png

View 观察 ViewModel 的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实 MVVM 的这一大特性并没有用到;

View 通过调用 ViewModel 提供的方法来与 ViewModel 交互;

View 通过 LiveData 来观察 ViewModel 的数据变化并自我更新,但这是单一数据源而不是双向数据绑定;

MVVM 相对 MVP 的

优点是:更彻底的解放了Activity,数据驱动是 MVVM 的关键词,ViewModel 不再主动调用方法去更新界面,而是主动更新数据,同时界面采用观察数据的方式,等待被更新,由于MVVM不需要写契约类Contract,也不存在Presenter层,也就不存在接口方法众多的问题;

缺点是:有问题的时候 不好找到原因;对外暴露的 LiveData 是不可变的,需要添加不勺模板代码且容易被遗忘;View 层与 ViewModel 层的交互比较分散零乱,不成体系;并且编译的时候会导致编译速度变慢,会额外生成ViewModel 文件;

这里额外插两个问题:

  • Jetpack 中的 ViewModel 和 MVVM 中的 ViewModel 它俩是一回事吗?

Jetpack 的 ViewModel 并不是 MVVM 中的 ViewModel,简单来说,MVVM 和 MVP 的最大区别在于数据绑定,而数据绑定本质上一种框架特性而非框架风格,区别在于数据绑定不能用一句简单的『建议大家使用』来推荐,只能由推荐方去实现这么一个功能,然后告诉大家『快用这个功能,你就能 MVVM 了』,也就是说,对于 Android 开发者来说,用了 JetPack 的 DataBinding 库,你才能叫用了 MVVM,光用 JetPack 的 ViewModel 是没用的,只要用了 DataBinding 库,基本架构也做了完美拆分(而不是把业务代码和界面逻辑全部放进 Activity),那么就算你不用 Jetpack 的 ViewModel,也是 MVVM (注意,不是勉强算 MVVM)而是确确实实的就是 MVVM。

  • 在 MVVM 模式中,ViewModel 是与 View 一一对应还是可以被其他 View 复用?

ViewModel 在 MVVM 中起到的作用是数据源,它内部包含的数据源恰好就是它所对应的 View 中所需要的数据。View 不需要对数据的获取做管理,也不需要对用户交互所导致的数据变化做管理,只用展示就可以;

所以,MVVM 中的 ViewModel 在原则是不被复用的;

同时,这里的 View 是 MVVM 中的 View,而不是 Android 系统的中的 View.java,每个 ViewModel 应该和每个 Activity/Fragment 做一一对应,并让它们在把数据分发到各个 View,

针对 MVVM 的缺陷,怎么办呢?变,MVI 来了;

MVI

说 MVI 之前,先来说两个概念,什么是响应式编程、命令式编程?

  • 响应式编程

持续性地赋值;响应式编程是一种面向数据流和变化传播的声明式编程范式 "数据流"和"变化传播"是相互解释的:有数据流动,就意味着变化会从上游传播到下游,变化从上游传播到下游,就形成了数据流。

  • 命令式编程

一次性赋值;

MVI 与 MVVM 比较相似,借鉴了前端框架思想,更加强调数据的单向流动和唯一数据源;

MVI 强调用响应式变成的方式进行事件到状态的变换,并且还得保证界面状态有唯一的可信的数据源,这样界面的刷新就形成了单向数据流,数据永远在一个环形结构中单向流动,不能反向流动;不管使用的是 ViewModel 还是 Presenter,MVI 关心的不是界面状态的持有者,而是整个更新界面数据链路的流动方式和方向;

image.png

单向数据流

单向数据流,界面变化是数据流的末端,界面消费上游产生的数据,并随上游数据的变化进行刷新;

用户操作以 Intent 的形式通知 Model;

Model 基于 Intent 更新 State;

View 接收到 State 的变化刷新 UI;

整个流程中包含两个数据:

数据一:从界面发出的事件(意图),即 MVI 中 I(Intent)。在 MVP 和 MVVM 中,界面发出的事件是通过一个 Presenter/ViewModel 的函数调用实现的,这是命令式的。为了实现响应式编程,需把这个函数调用转换成一个数据,即 Intent。

数据二:返回给界面的状态,即 MVI 中的 M(Model),它通常被称为状态 State,从字面就可以感觉到界面状态是会时刻发生变化的;

MVI 相对 MVVM 的

优点是:强调数据单向流动,很容易对状态变化进行跟踪和回溯;State 的集中管理,只需要订阅一个 ViewState 就可以获取界面的所有状态;

基于 MVI + Flow 的实战登录模块

image.png

intent 模块定义了 LoginViewEvent 用来分发数据,LoginViewState 用来响应数据,LoginViewAction 来更新 ViewState;

sealed class LoginViewEvent {data class ShowToast(val message: String): LoginViewEvent()object ShowLoadingDialog: LoginViewEvent()object DismissLoadingDialog: LoginViewEvent()
}

LoginViewState

data class LoginViewState(val userName: String= "", val password: String = "") {val isLoginEnable: Booleanget() = userName.isNotEmpty() && password.length >= 6val passwordTipVisible: Booleanget() = password.length in 1..5
}

LoginViewAction

sealed class LoginViewAction {data class UpdateUserName(val userName: String): LoginViewAction()data class UpdatePassword(val password: String): LoginViewAction()object Login: LoginViewAction()
}

我们使用 ViewModel 来承载 MVI 的 model 层,总结结构和 MVVM 也比较类似,主要区别在于 Model 与 View 的交互部分;

LoginViewModel 中承载 UI 状态,并暴露 ViewState 供 View 订阅;

View 层通过 LoginViewAction 更新 LoginViewState;

class LoginViewModel: ViewModel() {private val _viewState =  MutableStateFlow(LoginViewState())val viewStates = _viewState.asStateFlow()private val _viewEvent = MutableSharedFlow<LoginViewEvent>()val viewEvents = _viewEvent.asSharedFlow()fun dispatch(loginViewAction: LoginViewAction) {when(loginViewAction) {is LoginViewAction.UpdateUserName -> {updateUserName(loginViewAction.userName)}is LoginViewAction.UpdatePassword -> {updatePassword(loginViewAction.password)}is LoginViewAction.Login -> {login()}}}private fun updateUserName(userName: String) {_viewState.value = _viewState.value.copy(userName = userName)}private fun updatePassword(passWord: String) {_viewState.value = _viewState.value.copy(password = passWord)}private fun login() {viewModelScope.launch(Dispatchers.Main) {flow {if (loginLogic()){emit("登录成功")} else {emit("登录失败")}}.flowOn(Dispatchers.IO).onStart {_viewEvent.emit(LoginViewEvent.ShowLoadingDialog)}.onEach {_viewEvent.emit(LoginViewEvent.DismissLoadingDialog)_viewEvent.emit(LoginViewEvent.ShowToast(it))}.catch {_viewState.value = _viewState.value.copy(userName = "")_viewState.value = _viewState.value.copy(password = "")_viewEvent.emit(LoginViewEvent.DismissLoadingDialog)_viewEvent.emit(LoginViewEvent.ShowToast(it.message.toString()))}.collect()}}private suspend fun loginLogic(): Boolean {viewStates.value.let {// 执行 http 请求if (it.userName == "Kobe" && it.password == "123456") {delay(2000)return true} else {return false}}}
}

image.png

登录页面也比较简单,就是两个输入框和一个登录按钮;

class LoginActivity: AppCompatActivity() {private val userName: EditText by lazy { findViewById(R.id.userName) }private val passWord: EditText by lazy { findViewById(R.id.passWord) }private val login: Button by lazy { findViewById(R.id.login) }private val pwdTips: TextView by lazy { findViewById(R.id.pwdTips) }private val viewModel: LoginViewModel = LoginViewModel()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_login)initView()initViewStates()initViewEvents()}private fun initView() {userName.addTextChangedListener {viewModel.dispatch(LoginViewAction.UpdateUserName(userName = it.toString()))}passWord.addTextChangedListener {viewModel.dispatch(LoginViewAction.UpdatePassword(password = it.toString()))}login.setOnClickListener {viewModel.dispatch(LoginViewAction.Login)}}private fun initViewStates() {viewModel.viewStates.let { states ->lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {states.map {it.userName}.distinctUntilChanged().collect{userName.setText(it)userName.setSelection(it.length)}}}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {states.map {it.password}.distinctUntilChanged().collect{passWord.setText(it)passWord.setSelection(it.length)}}}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {states.map {it.isLoginEnable}.distinctUntilChanged().collect {login.isEnabled = itlogin.alpha = if (it) 1f else 0.5f}}}lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {states.map {it.passwordTipVisible}.distinctUntilChanged().collect {pwdTips.visibility = if (it) { View.VISIBLE } else { View.GONE }}}}}}private fun initViewEvents() {viewModel.viewEvents.let {lifecycleScope.launchWhenStarted {it.collect {when(it) {is LoginViewEvent.ShowLoadingDialog -> {showLoadingDialog()}is LoginViewEvent.DismissLoadingDialog -> {dismissLoadingDialog()}is LoginViewEvent.ShowToast -> {Toast.makeText(this@LoginActivity, it.message, Toast.LENGTH_SHORT).show()}}}}}}private var progressDialog: ProgressDialog? = nullprivate fun showLoadingDialog() {if (progressDialog == null)progressDialog = ProgressDialog(this)progressDialog?.show()}private fun dismissLoadingDialog() {progressDialog?.takeIf { it.isShowing }?.dismiss()}
}

好了,今天 NVx 就到这里吧,感谢您的观看

下一章预告


基于 MVI 的一个新闻列表客户端实战;

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/23395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kali扩容

通过wmware虚拟机–>设置–>添加40G容量的硬盘。 ──(root㉿kali)-[~/桌面] fdisk -lDisk /dev/sda: 40 GiB, 42949672960 bytes, 83886080 sectors …

Java多线程-初阶1

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:MySQL数据库 JavaEE专栏:JavaEE 关注博主带你了解更多数据结构知识 1. 认识线程&#xff08;Thread&#xff09; 1.线程是什么 ⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代…

计算机图形学入门07:光栅化中的采样与走样

1.什么是光栅化&#xff1f; 在前面的章节里提过&#xff0c;光栅化(Rasterization)就是将物体投影在屏幕上的图形&#xff0c;依据像素打散&#xff0c;每一个像素中填充不同的颜色。 如下图中的老虎&#xff0c;可以看到屏幕上有各种多边形&#xff0c;这些多边形经过各种变换…

教师办公神器推荐,口碑爆棚!

亲爱滴老师们&#xff0c;我要给大家安利几款超级实用的教师办公神器&#xff0c;不仅功能强大&#xff0c;而且口碑爆棚&#xff0c;绝对能让你们的工作效率飞起来&#xff01; 博思白板&#xff1a;创意无限&#xff0c;教学生动 一款可以无限扩展的数字白板&#xff0c;让教…

Lodop 实现局域网打印

文章目录 前言一、Lodop支持打印的方式lodop 打印方式一般有3种&#xff1a;本地打印局域网集中打印广域网AO打印 二、集成步骤查看lodop 插件的服务端口&#xff1a;查看ip后端提供接口返回ip&#xff0c;前端动态获取最后步骤 前言 有时候会根据不同的ip来获取资源文件&…

linux网络 dns域名解析

目录 DNS 域名体系结构 如何实现域名解析 正向解析 反向解析 主从服务器解析 bond 网卡 DNS 是域名系统的简称 域名和ip地址之间的映射关系 互联网中 IP地址是通信的唯一标识 逻辑地址 访问网站 域名 IP地址不好记 域名朗朗上口 好记 域名解析的目的就是为了实现 访…

为什么我们需要在软件本地化过程中使用术语服务?

你知道软件翻译和本地化的术语服务吗&#xff1f;此解决方案涵盖源术语和目标术语的创建、开发和维护。所有术语都存储在具有多个字段的数据库中&#xff0c;包括术语定义、用法示例、上下文和历史记录。这使我们能够正确处理每个术语的创建或更改请求&#xff0c;避免创建重复…

【ai】pycharm远程ssh开发

选 远程开发&#xff1a; SSH 连接到服务器 局域网的ubutnu 服务器的 ssh登录账号 登录后就开始下载 可能ubutnu 服务器上安装一个serverwindows 客户端的pychramr上装一个client &#xff1f; 突然发现实际有两个下载&#xff1a; 后端也要安装&#xff0c;竟然需要1G 那…

网站调用Edge浏览器API:https://api-edge.cognitive.microsofttranslator.com/translate

Edge浏览器有自带的翻译功能&#xff0c;在运行pc项目可能会遇到疯狂调用Edge的API https://api-edge.cognitive.microsofttranslator.com/translate 这个URL&#xff08;https://api-edge.cognitive.microsofttranslator.com/translate&#xff09;指向的是微软认知服务中的A…

【问题解决】adb remount 失败或刷机无法连接设备(KaiOS)

问题描述 1、设备无法adb remount成功&#xff0c; 2、通过fastboot无法识别设备&#xff0c;一直卡住 3、已经识别到9008端口&#xff0c;但是设备与刷机工具connect fail&#xff0c;甚至软件crash 解决方案 1、安装高通驱动工具&#xff1a;QDLoder HS-USB Driver QDLoade…

【Python】【matLab】模拟退火算法求二元高次函数最小值

一、目标函数 求二元高次函数的最小值。目标函数选择&#xff1a; 用于测试算法的简单的目标函数&#xff1a; 二、Python代码实现 import numpy as np# 目标函数&#xff08;2变量&#xff09; def objective_function(x):return x[0] ** 2 2 * x[0] - 15 4 * 4 * 2 * x[…

【Vue】项目目录介绍和运行流程

文章目录 一、项目目录介绍二、public/index.html三、src/main.js四、运行流程 一、项目目录介绍 虽然脚手架中的文件有很多&#xff0c;目前咱们只需认识三个文件即可&#xff0c;这三个文件就决定了我们项目的运行 main.js 入口文件App.vue App根组件index.html 模板文件 我…

递归在多级数据结构中的简单应用

哈喽&#xff0c;我是小码&#xff0c;半年多没更新了&#xff0c;这段时间换了新工作&#xff0c;工作也很忙。后续会尽量多写点&#xff0c;坚持确实是一件很难&#xff0c;很酷的事情。最近在公司负责开发商品有关的开发&#xff0c;商品包含类型、款式等属性&#xff0c;而…

视频推广短信:新时代的营销利器(视频短信XML接口示例)

随着移动互联网的普及&#xff0c;短信已经不再是简单的文字信息传递工具&#xff0c;而是逐渐演变为一种有效的推广手段。特别是当视频与短信结合时&#xff0c;它所带来的营销效率更是令人瞩目。 一、视频推广短信的特点 1.直观性&#xff1a;与传统的文字短信相比&#xf…

游戏盾之应用加速,何为应用加速

在数字化时代&#xff0c;用户对于应用程序的防护要求以及速度和性能要求越来越高。为了满足用户的期望并提高业务效率&#xff0c;应用加速成为了不可忽视的关键。 应用加速是新一代的智能分布式云接入系统&#xff0c;采用创新级SD-WAN跨域技术&#xff0c;针对高防机房痛点进…

令人瞠目结舌的8个ChatGPT-4o提示词

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

【python】OpenCV—Bitplane

学习来自&#xff1a; 位平面分割&#xff08;Bit-Plane Slicing&#xff09;使用OpenCVPython进行图像处理的初学者指南 位平面 位平面&#xff08;bitplane&#xff09;是一个在计算机科学中用于描述图像数据的概念&#xff0c;具体定义如下&#xff1a; 【定义】&#x…

《手把手教你》系列练习篇之10-python+ selenium自动化测试(详细教程)

1. 简介 今天我们继续前边的练习&#xff0c;学习和练习一下&#xff1a;如何使用webdriver方法获取当前测试页面的URL、如何获取当前页面的title、如何打开浏览器的一个新建页面、如何操作单选按钮等等&#xff0c;这些小练习&#xff0c;来巩固基础。 2. webdriver方法获取…

​谁用谁知道,教师实用工具分享​

老师们面临着日益增长的教学和管理任务。为了有效提升工作效率&#xff0c;一些实用的工具成为了老师们不可或缺的助手。给大家分享几款教师必备的工具&#xff0c;帮助教师们在教学和管理工作中更加得心应手。 1. 知乎&#xff1a;知识的海洋 知乎是一个中文问答社区&#xf…

Docker-compose安装、使用,容器化部署springboot项目

一、docker-compose的安装 1、按官网的方式去安装 1&#xff09;下载docker-compose curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 若…