前言
本章主要基于以下几个方向进行 MVx 的讲解,带你玩转 MVx;
MVC、MVP、MVVM、MVI 它们到底是什么?
分文件、分模块、分模式
一个文件打天下
为什么不要用一个页面打天下?
页面是给用户看的,随着版本的迭代,页面上的交互需求也在不断的变化,导致页面不断的在修改,页面在不断的修改,那么这个文件中就不能放太多的代码,否则导致后续迭代起来就很困难;
页面很复杂怎么办?
一个复杂页面,也是由一个一个的 View 组成的,那么我们就可以将 View 拆成自定义 View。同时将这个 View 的相关逻辑,拆分到对应的逻辑处理层;
应用开发原则
- 遵循面向对象的SOLID原则;
- 视图、数据、逻辑分离;
六大原则
- 单一职责
一个 class 完成一件事情;一个 class 只做一件事情,当有更多事情的时候,使用继承,那么就引申出了『开闭原则』;
- 开闭原则
对继承开放,对修改关闭(不能改变基类中的逻辑);多个类文件完成多件事情的时候,使用继承,对文件尽量不做修改;
- 里氏替换原则
子类覆写父类函数的时候,不能改变父类的逻辑;例如:「基类车 有一个 run 方法,子类覆写之后不能改成 fly 方法」
- 依赖倒置原则
不依赖实现,只依赖接口;「例如车在路上跑,车不应该依赖路的具体实现,而是一个路的接口」;
- 接口隔离原则
接口的粒度要小,接口的最小化;『例如,人和路的接口要分别定义,而不是定义成一个接口』;
- 迪米特原则
最小支持原则;
视图、数据、逻辑分离
从静态角度的一个分离,以及生命周期的控制,数据需要在什么情况下进行销毁;
App 架构设计
整体遵循一个 层次化、模块化、控件化;
而模块化又可以细分成:组件化、插件化;
组件化一般针对的都是业务组件,业务组件之间互相不依赖,公共部分抽象成接口,下沉到通用组件;组件化的意义是通过 App 模块把这些组件拼装起来,组装成一个 APK,这种就是在编译期间,它们是一个一个的组件;
而插件化,本质上可以理解为,把这一个一个的组件打包成一个一个的无图标的 APK,然后通过后下载的方式集成到应用中,以此来缩小安装包的体积;
MVx
在 MVx 中,这个 M 基本都是一样的,V 的角色其实是有一些差异的;
MVC
数据层、视图层、控制层(逻辑层)如何定义?
- 数据层
对数据 + 对数据进行的操作(不依赖视图的操作);例如我们利用 RxJava 对数据进行变换的操作,map、flatMap等等,放到 model 层执行;
- 视图层
不同的模式有不同的定义,在 Android 中就是 Activity + xml + fragment;
- 控制层
view 和 model 之间的通信和交互;
在 Andriod 中 xml 布局的功能性太弱, Activity 实际上负责了 View 层与 Controller 层两者的工作,所以在 Android 中 MVC 更像是这种形式,而 Model 层好多人直接把 bean 写在了 model 下,它俩并不能完全相等;
MVC 相对一个文件打天下
优点是:抽离了 model;
缺点是:controller 的权力太大,在 Android 中 Activity 承担了 Controller 的角色,如果这个 Activity 页面逻辑太多的话,这个 Activity 依然会变得特别臃肿,就又回到了一个文件打天下的场景了;
那么,怎么办呢?变! MVP 来了
MVP
- 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 给干掉!
可以看出 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 来实现,否则就会变成下面的这种
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 关心的不是界面状态的持有者,而是整个更新界面数据链路的流动方式和方向;
单向数据流
单向数据流,界面变化是数据流的末端,界面消费上游产生的数据,并随上游数据的变化进行刷新;
用户操作以 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 的实战登录模块
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}}}
}
登录页面也比较简单,就是两个输入框和一个登录按钮;
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 的一个新闻列表客户端实战;
欢迎三连
来都来了,点个关注,点个赞吧,你的支持是我最大的动力