如何应对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,一经查实,立即删除!

相关文章

6.6小结

Problem - A - Codeforces 思路&#xff1a; 一次最多只能走一步或者两步&#xff0c;只需要判断后面两个是不是都是*就行 #include<bits/stdc.h> using namespace std; char a[1010]; int main() {int t;cin >> t;while (t--){int n, flag0;int ans 0;cin >…

kali扩容

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

DevOps的原理及应用详解(二)

本系列文章简介&#xff1a; 在当今快速变化的商业环境中&#xff0c;企业对于软件交付的速度、质量和安全性要求日益提高。传统的软件开发和运维模式已经难以满足这些需求&#xff0c;因此&#xff0c;DevOps&#xff08;Development和Operations的组合&#xff09;应运而生&a…

Qt5学习笔记

一、基础知识 1、基本控件类型 水平弹簧与垂直弹簧的父类都是QSpaceItem。关于PushButton相关的控件类型&#xff1a; QPushButton&#xff1a;最基础的按钮类型。QToolButton&#xff1a;可以控制图片、文字任意组合的显示方式的按钮类型。QRadioButton&#xff1a;就像rad…

Java多线程-初阶1

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

高并发数据处理中心服务器设计

涉及的相关框架Spring Cloud、RabbitMQ、Redis 和 MySQL&#xff1b; Spring Cloud&#xff1a;用于微服务的开发&#xff0c;确保服务间的通信和协作。 RabbitMQ&#xff1a;用于异步消息队列&#xff0c;确保系统的高可用性和扩展性。 Redis&#xff1a;用作缓存&#xff…

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

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

ARM功耗管理架构演进及变迁

安全之安全(security)博客目录导读 目录 一、功耗管理架构演进及变迁概述 二、多核 三、big.LITTLE 四、DynamIQ

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

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

Lodop 实现局域网打印

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

Github 最新的上传代码操作步骤

github取消了密码账号授权登录方式 原有的link到github账户后&#xff0c;还需要进行令牌的授权 首先需要创建一个令牌。可以参照以下链接 创建好令牌之后&#xff0c;以后需要执行 git remote set-url origin https://[令牌代码]github.com/xxx/xxx/完整的操作流程如下&am…

linux网络 dns域名解析

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

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

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

docker容器内时区相差8小时问题

docker容器内时区相差8小时问题&#xff1a; 提示&#xff1a;java 时间记录差*小时&#xff1a; 进入容器内修改&#xff08;已经实践过的&#xff09; &#xff08;1&#xff09;更改容器时间 也就是操作的文件夹是容器内的文件夹&#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[…

各家AI大胆帮我预测一下2024年全国高考语文作文 并模拟出题 坐看AI算的准不准

chatgpt-4o 抱歉&#xff0c;我无法预测具体的高考题目。不过&#xff0c;我可以根据近年的趋势和主题为你模拟一个可能的作文题目&#xff1a; 2024年全国高考语文作文模拟题目&#xff1a; 题目&#xff1a;《共同的家园》 要求&#xff1a; 以“共同的家园”为主题&…

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

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