MVC

Model View Controller 由三部分组成 (M-V-C):

  • Model:数据模型,管理数据状态(Bean、Entity)

  • View:可视化界面,用于展示数据

  • Controller:控制者,负责处理用户与应用之间的交互,包括业务逻辑;

MVC中,View和Model可以直接通信,增加了其耦合!(缺点)

Android 中, XML 编写的界面就是 View 层,但是 XML 对 UI 的控制太差(只是用来声明界面长啥样,而不能控制界面做什么),因此作为 XML 界面的承载体 Acitivty / Fragment 就需要承载 View 的更新控制职责;

Acitivity/Fragment 既作为 Controller 层也作为 View 层:

  • Activity 需要根据用户的输入事件去执行相关操作;

  • 比如点击按钮发送网络请求就需要:

    1. Button.setOnClickLisenter (作为 View 层响应用户输入)

    2. 在其内部调用 fetchNetworkData(作为 Controller 层通知 Model 层更新数据)

    3. 根据拿到的数据更新 UI 界面 TextView.setText(xxx)(作为 View 层更新界面)

如果开发不规范,甚至可能在 Activity 中编写 Model 层处理数据的操作(比如网络请求拿到数据后转成VO的操作),就会导致三层都集中在 Activity 中,代码骤增,其耦合性也是非常高的;

Android实现 MVC 的大致架构如下:

  • Activity 既作为 Controller 也作为 View 层:在 Activity 中,同时存在访问 Model 层、更新UI的代码


MVP

Model-View-Presenter 由三部分组成:

  • Model:数据模型,管理数据状态;

  • View:可视化界面,负责数据的展示和与用户的交互;

  • Presenter:主持者,Presenter 通过 View 接收用户的输入,通知 Model 层处理数据并将处理结果传递回 View;

相较于 MVC 架构模式,MVP 架构模式最大的变化就是将 View 和 Model 的依赖关系删除,让 Presenter 作为 View 和 Model 之间的桥梁:

  • Presenter需要接收 View 传递的事件并进行处理后,通知 Model 更新数据

  • Model 更新完毕数据后,通知 Presenter 更新完毕,Presenter 通知 View 层更新;

缺点:

  • 需要定义大量的接口规定 View 层和 Model 层的方法,会引入大量的文件;同时随着业务逻辑的增多,需要更多的接口进行回调;

  • 因为 Persenter 作为 View层 和 Model 层的中转站,所以 Persenter 需要持有 View 层(Activity)和Model层的引用,如果处理不当(生命周期)会导致内存泄漏;

Android 的 MVP:

  • 单独实现 XxxxPresenter,内部带有对 Acitivty (View层)的引用

     class XxxxPresenter: IXxxPresenter {
         private val xxxModel: IXxxModel // Model 层的引用
         private val xxxView: IXxxView // View 层的引用
         
         // 对 xxxModel 的调用就是处理数据
         // 对 xxxView 的调用就是通知 View 更新
     }
  • Activity 主要负责 View 层的更新

     class MainActivity: AppCompatActivity(), IXxxView {
         private val xxxPresenter: XxxPresenter
         // 与 Presenter 层交互
     }
  • Model 层负责数据的处理和通知:

     class XxxModel: IXxxModel {
         // 更新数据,更新完数据后,还需要通知 Presenter
         private val presenter: IxxxPresenter
     }

与 MVC 最大的变化就是:

Activity 不再担任 Controller,而是将其独立出去,变成为 Presenter 层;让 Activity 专于 View 的更新操作;


MVVM

由三个部分组成 (M-V-VM):

  • Model(模型): 表示应用程序的数据模型或业务逻辑,负责存储数据的存储、处理和操作(数据结构、数据库操作和网络请求等)

  • View(视图):可视化界面,用于展示数据并与用户进行交互;

  • ViewModel(视图模型):

    • 是 View 和 Model 之间的桥梁,负责将数据从 Model 中取出并转换成 View 可用的形式

    • 并不直接操作UI,而是通过数据绑定机制与 View 进行绑定,使得数据更新时让 UI 自动同步;

    • ViewModel 也包含用户交互的逻辑(比如处理用户输入、点击事件等)

image-wwqa.png

主要目标是将应用的UI与其底层数据模型分离,通过数据绑定实现数据和UI的自动同步,从而降低代码的耦合度;

  • View 负责与 ViewModel 中的数据绑定, 进行UI的更新,并将事件通知给 ViewModel;

  • ViewModel 负责处理 View 的事件,并通知 Model 更新数据,Model更新数据后通知给 ViewModel, ViewModel 更新对应的数据,因为数据绑定,View层自动更新UI;

上面的架构图似乎和 MVP 相同,但是具体实现不同:

  • MVP是通过接口回调的形式,被动的接收 Persenter 层传递的数据;

  • MVVM是以观察者的形式,通过观察数据变化来主动更新UI;

MVVM 的核心:

  • 数据绑定: 将 View 和 ViewModel 的数据同步连接,当 ViewModel 的数据发生变化时,数据绑定会自动更新 View 绑定这些数据的部分;

  • 双向数据绑定:数据的更新实时反映到UI上,UI对数据的操作也能更新 ViewModel 的数据;

在 Google 的曾经推荐过的架构如下图:

image-diwm.png

ViewModel 负责界面数据的存储、同步、更新操作,同时负责访问 Model 的数据等业务逻辑;

View 主要是 Activity / Fragment 所呈现的 UI 界面,使用 DataBinding/LiveData/StateFlow 等支持观察者模式的数据结构来实现数据绑定;

Model 中统一由 Repository 层负责所有数据的获取和更新,数据的来源又分为本地数据库和远程服务器拉取的数据;


MVI

现在谷歌目前所推荐的架构(Now In Android 用的是这个架构);

M-V-I由三部分组成:

  • Model:和先前的架构都不同,这里指的是界面状态,比如页面加载状态、组件可用状态等,也负责数据处理;

  • View:可视化界面,可以接收 Model的状态变化,通过 Intent 事件对界面进行变化;

  • Intent:(不是系统的Intent)将用户的操作包装为 Intent 事件,交由 Model 层进行处理

我个人认为类似 有限状态机

将界面划分为多个State,将操作划分为多个Intent;

Intent可以触发 State 的变换,形成完整的状态流转;

Android 代码大致实现:

  1. 定义界面状态,这里的ApiResponseState为获取API数据相关的界面状态:

     // 使用 Kotlin 提供的 sealed 关键字能够很好的适配这种有限状态;
     sealed interface ApiResponseState<out T> {
     ​
         data object BeforeRequest: ApiResponseState<Nothing>
         
         data object Loading : ApiResponseState<Nothing>
     ​
         data class NetworkError(val errMsg: String, val errCode: String) : ApiResponseState<Nothing>
     ​
         data class Success<T>(val data: T) : ApiResponseState<T>
     }
  2. 定义 ViewModel,用于处理数据以及更换状态:

     class ExampleViewModel: ViewModel {
         private val _uiState = MutableStateFlow<ApiResponseState<InfoBean>>(ApiResponseState.BeforeRequest)
         val uiState = _uiState.asStateFlow()
         
         private fun fetchInfoBean() {
             // 界面进入加载状态
             _uiState = ApiResponseState.Loading
             viewModelScope.launch(Dispatchers.IO) {    
                 runCatching {
                     repository.getInfoBean()
                 }.onSuccess {
                     // 界面进入加载成功状态
                     _uiState.value = ApiResponseState.Success(it)
                 }.onError {
                     // 界面进入加载失败状态
                     _uiState.value = ApiResponseState.NetworError(it.msg, /* custom code */)
                 }
             }
         }
     }
  3. 根据用户的操作,划分为多个Intent,类似 State

     sealed interface UserIntent {
         data object StartRequest: UserIntent
         data object Refreshing: UserIntent
         // ....
     }

    然后在 ViewModel 中根据事件来调用对应的方法,进行数据处理

     fun dispatchUserIntent(intent: UserIntent) {
         when(intent) {
             StartRequest -> {
                 fetchInfoBean()
             }
             Refrehsing -> {
                 refreshInfoBean()
             }
             // ....
         }
     }
  4. 最后在 View 层监听 Model 状态的变化,并执行对应的 UI 更新,以及响应用户的事件并交由ViewModel处理

     lifecycleScope.launch {
         viewModel.uiState.collect { state ->
             when(state) {
                 ApiResponseState.Loading -> {
                     // 加载状态对应的UI
                 }
                 is ApiResponseState.Success -> {
                     state.data // 数据
                     // 加载成功对应的UI              
                 }
                 ApiResponseState.Network -> {
                     // 加载失败对应的UI
                 }
             } 
         }
     }
     ​
     // 响应用户输入的事件
     viewBinding.refreshingButton.setOnClickLisenter {
         // 执行刷新操作
         viewModel.dispatchUserIntent(UserIntent.Refreshing)
     }

MVI 适合 Compose

对于上面的 State 来说,更多的情况是数据某一部分发生变化(比如 copy 修改某一部分的值),因为 Compose 支持差量更新(仅支持数据发生变化的部分);

但是对于传统的 View 系统来说:

 lifecycleScope.launch {
     viewModel.uiState.collect { state ->
         // 更新所有与 state 相关的UI控件
     }
 }

每次 State 发生更新时,都会更新与其相关的UI控件,出现上面的数据某一部分发生变化的场景,这种写法会导致数据没有发生变化,但是发生刷新的问题,造成刷新过度的问题;

如果需要避免这个问题,就需要独立做差量更新(比如判断与先前的值是否相同的操作),而 Compose 框架本身就带有差量更新,就不用手动编写这部分代码;




UseCase

谷歌所推荐的应用架构中,包含了几个原则:

  • 分离关注点

  • 数据模型驱动UI更新

  • 单一数据源SSOT:数据只有一处来源,数据的获取、更新都只能从来源处执行,或者说相关操作由来源处提供;

  • 单向数据流 UDF:数据状态仅从一个方向流动(往UI层),而修改数据的事件往反方向流动

因此将应用划分为:

  • UI层:Activity/Compose + ViewModel(存State)

  • Domain 层:可选,Data 层到 UI 层的数据处理逻辑

  • Data 层:数据源,负责数据加载、更新、持久化;

相较于之前的 MVVM 架构中的 ViewModel 需要包含大量的业务逻辑和处理 View 层的事件输入代码,Domain 层将 ViewModel 中复杂的业务逻辑或者重复使用的业务逻辑抽离封装,减少 ViewModel 的职责;

在 Domain 层中,推荐使用 UseCase 作为实现方式:

  • 不持有UI状态:为了保证 SSOT 和 UDF,不需要根据状态来调整自身逻辑;

  • 单一职责:一个 UseCase 仅做一件事;

大致代码:

 class GetInfoBeanUseCase constructor(
     private val dataRepository: DataRepository
 ) {
     operator fun invoke(/* 所需要的数据 */): Flow<InfoBean> {
         return dataRepository.getInfoBeanCase(...).map{ /* 负责处理获取到的数据 */}
         /* 将这些业务逻辑从 ViewModel 抽离出来 */
     }
 }