从 MVC 到 MVIAndroid 架构到底在解决什么问题1. 引入一个2000行的Activity你可能见过或者写过这样的代码publicclassMainActivityextendsAppCompatActivity{// 网络请求privateOkHttpClientclient;// 数据库privateSQLiteDatabasedb;// 业务逻辑privatevoidloadData(){/* 300行 */}// UI更新privatevoidupdateUI(){/* 200行 */}// 点击事件publicvoidonClick(Viewv){/* 500行 */}// ...}一个Activity里塞了网络请求、数据库操作、业务判断、UI更新、事件处理……什么都干了什么都干不好。改一个需求牵一发而动全身想写单元测试发现根本没法测两个人同时改这个文件天天冲突。架构要解决的就是这个问题把一坨代码拆成各司其职的几块。但问题来了——怎么拆拆成几块块和块之间怎么通信这几十年来Android架构一直在回答这三个问题。2. MVC最早的拆分尝试2.1 MVC长什么样MVC把代码分成三块角色职责Android中的对应Model数据和业务逻辑数据类、数据库、网络请求View显示界面XML布局Controller接收用户输入协调Model和ViewActivity2.2 问题出在哪MVC在桌面端工作得很好但搬到Android上就变味了。原因很简单在Android里Activity既是Controller又是View。publicclassMainActivityextendsAppCompatActivity{// 这是ViewprivateButtonbtnLoad;publicvoidonClick(Viewv){// 这是Controller的活// 但这里又能直接操作ViewbtnLoad.setText(加载中...);// 又能直接调ModelStringdataloadFromDatabase();// 又能更新UIbtnLoad.setText(data);}}Activity既当爹又当妈Controller和View的边界彻底模糊了。结果就是Activity越来越胖最后变成那个2000行的怪物。MVC在Android上的核心问题View和Controller没有真正分离。3. MVP让Activity只当View3.1 MVP怎么改MVP的思路很直接把Controller从Activity里抽出来变成一个独立的Presenter。角色职责Android中的对应Model数据和业务逻辑数据类、RepositoryView显示界面 定义UI接口Activity/Fragment实现接口Presenter业务逻辑协调Model和View独立类与MVC核心区别MVCMVP协调者角色Controller 接收输入、协调 Model/ViewPresenter 接管全部表现逻辑View 地位View 可能直接观察/读取 Model不是完全被动View 是“哑巴”只通过接口被动显示通信方式Controller 与 View 界限常模糊View ↔ Presenter 通过接口双向通信谁决定显示什么有时 View 自己根据 Model 刷新Presenter 明确调用view.showXxx()可测试性Controller 常依赖框架难纯单元测试Presenter 不依赖 Android可纯 JVM 测试Android 典型问题Activity 兼任 ViewController胖 Activity逻辑抽到 PresenterActivity 只实现 View 接口关键设计View和Presenter通过接口通信。// View接口定义能做什么UI操作publicinterfaceIUserView{voidshowLoading();voidshowData(Stringdata);voidshowError(Stringmsg);}// Presenter持有View接口publicclassUserPresenter{privateIUserViewview;publicvoidloadUser(){view.showLoading();Stringdatamodel.getUser();// 调Modelview.showData(data);// 通知View更新}}// Activity只负责实现接口publicclassMainActivityextendsAppCompatActivityimplementsIUserView{privateUserPresenterpresenter;OverridepublicvoidshowLoading(){progressBar.setVisibility(VISIBLE);}OverridepublicvoidshowData(Stringdata){textView.setText(data);}}3.2 解决了什么Activity变薄了只负责显示业务逻辑抽到Presenter里Presenter可以单独做单元测试因为View是接口可以Mock3.3 带来了什么新问题问题一接口爆炸。每个页面都要定义一个View接口方法越加越多接口越来越臃肿。问题二Presenter持有View引用。如果页面销毁了Presenter还在就会内存泄漏如果Presenter异步回调时View已经销毁就会空指针。问题三手动管理生命周期。Presenter要在onDestroy里解绑View每个页面都要写一遍容易忘。MVP的核心问题View和Presenter耦合太紧生命周期管理全靠手动。4. MVVM让数据驱动UI4.1 MVVM怎么改MVVM的思路是让View和业务逻辑彻底解耦通过观察数据来自动更新UI。角色职责Android中的对应Model数据和业务逻辑RepositoryView显示界面观察ViewModelActivity/FragmentViewModel持有UI状态暴露可观察数据ViewModel类关键变化View不再被动等Presenter调用而是主动观察ViewModel里的数据。// ViewModel暴露可观察的数据publicclassUserViewModelextendsViewModel{privateMutableLiveDataStringuserDatanewMutableLiveData();privateMutableLiveDataBooleanloadingnewMutableLiveData();publicLiveDataStringgetUserData(){returnuserData;}//暴露数据publicvoidloadUser(){loading.setValue(true);Stringdatarepository.getUser();userData.setValue(data);// 数据变了UI自动更新loading.setValue(false);}}// View观察数据publicclassMainActivityextendsAppCompatActivity{OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);viewModel.getUserData().observe(this,data-{textView.setText(data);// 数据变了这里自动执行});viewModel.getLoading().observe(this,isLoading-{progressBar.setVisibility(isLoading?VISIBLE:GONE);});}}4.2 解决了什么生命周期自动管理observe(this, ...)会自动感知生命周期页面销毁自动解绑不会内存泄漏配置变更不丢数据ViewModel在屏幕旋转时不会被销毁数据还在数据驱动UIUI是数据的函数数据变了UI自动跟着变4.3 带来了什么新问题问题一状态分散。一个页面的状态可能散落在多个LiveData里loading、data、error、page……组合起来容易漏、容易冲突。问题二数据流混乱。ViewModel里的数据可以随便改View也可以直接调ViewModel的方法改数据两边都能改出了问题不好追。问题三一次性事件难处理。Toast、导航这类只发生一次的事件用LiveData表示很别扭旋转屏幕会重复触发。问题四状态恢复困难。页面被杀后重建要恢复所有状态散落的LiveData不好统一管理。MVVM的核心问题数据流是双向的状态是分散的出了问题很难定位。5. MVI单向数据流状态唯一5.1 MVI怎么改MVI的核心思想只有两条状态只有一个单一可信源数据只能单向流动Intent → 处理 → 新状态 → UI角色职责说明Model数据 状态一个包含页面所有状态的data classView渲染状态 发出Intent只读状态只能通过Intent表达意图Intent用户意图点击、输入、刷新等操作Reducer处理Intent生成新状态纯函数旧状态 Intent 新状态5.2 核心变化状态只有一个// 整个页面的状态一个类装下dataclassUserState(val loading:Booleanfalse,val data:String?null,val error:String?null,val page:Int1)// ViewModel只暴露一个StateclassUserViewModel:ViewModel(){privateval _stateMutableStateFlow(UserState())val state:StateFlowUserState_state// 所有操作都通过Intent触发funsendIntent(intent:UserIntent){when(intent){isUserIntent.LoadUser-loadUser()isUserIntent.Refresh-refresh()}}privatefunloadUser(){_state.value_state.value.copy(loadingtrue)// 改状态// ... 请求数据_state.value_state.value.copy(loadingfalse,dataresult)}}// View只做两件事渲染状态 发IntentviewModel.state.collect{state-// 根据状态渲染UIprogressBar.isVisiblestate.loading textView.textstate.data errorView.isVisiblestate.error!null}button.setOnClickListener{viewModel.sendIntent(UserIntent.LoadUser)// 只发意图不改状态}5.3 为什么这样更好问题MVVMMVI状态分散多个LiveData一个State类数据流双向单向状态冲突可能不一致不会只有一个源问题定位难追看Intent和State变化就知道状态恢复手动保存State即可可测试性一般好Reducer是纯函数5.4 MVI的代价样板代码多每个页面都要定义State、Intent、Reducer学习成本高单向数据流、StateFlow、协程对新手不友好简单页面过度设计一个只有列表的页面用MVI反而累赘状态类可能膨胀页面复杂时State类会变得很大6. 四种架构对比维度MVCMVPMVVMMVI核心思想职责三分抽离Presenter数据驱动UI单向数据流View职责又当C又当V实现View接口观察ViewModel渲染状态发Intent数据流乱双向接口双向LiveData单向状态管理无无分散在多个LiveData单一State生命周期手动手动自动ViewModel自动ViewModel可测试性差好较好好样板代码少多接口中多State/Intent适用场景小项目中型项目大部分项目复杂状态页面7. 架构到底在解决什么问题回到标题的问题。从MVC到MVI架构不断演进本质上一直在解决三个问题7.1 职责分离让每一块代码只干一件事。MVC没做好Activity又当C又当VMVP做好了Presenter独立MVVM做得更好ViewModel独立且生命周期安全MVI做得最彻底View只渲染逻辑全在ViewModel7.2 状态管理让页面的状态变得可预测、可追踪。MVC/MVP状态散落在各处谁改了不知道MVVM状态在ViewModel里但分散在多个LiveData组合起来容易出问题MVI所有状态收进一个State类只有一个可信源7.3 可测试性让业务逻辑脱离UI也能测。MVC逻辑在Activity里测不了MVPPresenter是普通类能测MVVMViewModel是普通类能测MVIReducer是纯函数最好测一句话总结架构的本质就是让代码可读、可维护、可测试。不同的架构方案只是在分离程度和复杂度之间找平衡。8. 怎么选项目情况推荐个人小项目、Demo不用架构或简单MVC中小型项目、团队协作MVVM最主流状态复杂的页面如播放器、聊天MVI老项目维护保持现状新页面用MVVM学习架构思想按MVC→MVP→MVVM→MVI顺序理解关键认知没有最好的架构只有最适合当前项目的架构。MVI不是什么银弹简单页面用MVI是自找麻烦。架构是为业务服务的不是为技术炫技服务的。9. 一句话总结从MVC到MVIAndroid架构一直在回答同一个问题怎么把逻辑和界面分开怎么让状态变得可控。MVC分得不彻底MVP分得太生硬MVVM靠数据驱动实现了自动解耦MVI用单向数据流把状态管理做到了极致。架构演进的本质是从人能管住代码到代码自己管住自己。
阅读完成 · 觉得有帮助?