Android开发中常见的MVP封装形式
一个缩略版的 MVP 结构——现在很少用了,但搞懂它有助于理解后来的 MVVM。
为什么还写 MVP
这篇是 当我们在谈MVP、MVVM、MVI的时候,我们到底在谈什么? 的缩略版。
说实话,实际业务里我很少再主动用 MVP 了——新项目直接上 MVVM(或者 MVI)更省心。但 MVP 在 Android 架构演进里占过很长一段,而且它和 MVVM 的对比,能帮你把「Presenter 手动绑生命周期」和「ViewModel 自动扛旋转屏」这两件事想明白。
下面这段 Kotlin 代码,是我觉得还算能代表 MVP 核心思路的一种封装:契约接口 + Presenter 拉数据 + View 只负责渲染。不展开讲基类、泛型封装、AutoService 自动注册那些——那些能让 MVP 好用一点,但封装成本上去了,不如直接 MVVM。
契约长什么样
MVP 里最常见的一个套路,是先定 IContract,把 View 和 Presenter 能干什么写清楚:
IViewRender:View 对外暴露的渲染能力,比如收到字符串之后刷新 TextViewIPresenter:业务入口,负责拉数据;同时要有attachView/detachView,把 View 的生命周期管起来
interface IContract {
interface IViewRender {
fun callback(message: String)
}
interface IPresenter {
fun fetchData()
// 绑定 View,Presenter 才知道往哪回调
fun attachView(viewRender: IContract.IViewRender)
fun detachView()
}
}接口多,是 Java 时代 MVP 常被吐槽「笨重」的原因之一。查一个业务的完整链路,往往要在 Contract、Presenter、View 几个文件之间跳。但反过来说,职责边界也写得比较死——适合当年那种「先把 Activity 里的逻辑往外搬」的诉求。
View 和 Presenter 怎么配合
View 侧(Activity / Fragment)实现 IViewRender,在合适的生命周期里挂上 Presenter、触发请求、收到回调后改 UI:
class MyView : IContract.IViewRender {
private val presenter: Presenter = Presenter()
override fun onViewCreated() {
presenter.attachView(this)
presenter.fetchData()
}
override fun onDestroy() {
presenter.detachView()
}
override fun callback(message: String) {
mBinding.tvInfo.text = message
}
}Presenter 侧持有 View 的弱引用(这里用可空变量示意),异步拿数据,完了通过 viewRender?.callback() 丢回去:
class Presenter : IContract.IPresenter {
private var viewRender: IContract.IViewRender? = null
private val mCompositeDisposable = CompositeDisposable()
override fun fetchData() {
Single.create<String> { emitter ->
Thread.sleep(2000L) // 模拟耗时请求
emitter.onSuccess("Hello World")
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : SingleObserver<String> {
override fun onSubscribe(disposable: Disposable) {
mCompositeDisposable.add(disposable)
}
override fun onSuccess(result: String) {
viewRender?.callback(result)
}
override fun onError(e: Throwable) = Unit
})
}
override fun attachView(viewRender: IContract.IViewRender) {
this.viewRender = viewRender
}
override fun detachView() {
mCompositeDisposable.clear()
viewRender = null
}
}上面两段拼在一起,就是 MVP 最典型的一条数据流:Presenter 访问上层拿 Data → 回调 View → View 刷 UI。业务逻辑从 Activity 里挪出去了,测试 Presenter 也比测一整坨 Activity 现实一些。
真正要命的是生命周期
很多团队封装 MVP,只做了「Presenter 能往 View 回调数据」这一半,attach / detach 写得马虎,或者干脆忘了在销毁时 clear() 掉 Rx 订阅。
于是常见一幕:页面已经 onDestroy 了,Presenter 里的网络请求还在跑;回调落地时,View 里的 mBinding 早就没了。Java 项目直接 NPE;Kotlin 有空安全,NPE 可能少了,但内存泄漏、无效刷新、偶发 ANR 照样会来。
所以示例里 onDestroy 调 detachView()、mCompositeDisposable.clear()、viewRender = null,不是可有可无的锦上添花——这是 MVP 能不能在生产环境站住脚的分水岭。当年我见过不少「名义上是 MVP、实际和 MVC 没差」的项目,问题多半出在这里,而不是 Presenter 代码多几行。
当然,完整版还可以把 onStart / onStop、配置变更、Presenter 是否该用 WeakReference 再细分一层。这篇故意缩略,那些生命周期细节就不展开了。
和 MVVM 怎么选
我自己的结论是:如果你今天开一个新模块,直接 MVVM 就行,不必在 MVP 的 Contract + 手动绑生命周期上再投入封装成本。Jetpack 的 ViewModel 自带配置变更存活,LiveData / Flow 又自带生命周期感知,省掉的是一整套「每个页面自己记得 detach」的心智负担。
MVP 的价值更多在「读懂老项目」和「理解架构为什么要往 MVVM 走」。想看我这边 MVVM 的写法,可以去 如何在Android开发中实现MVVM的架构 那篇。
这篇就当作一个能跑的 MVP 最小样本留着。代码不漂亮,但够说明问题——把数据生成挪到 Presenter,把 UI 刷新留在 View,中间用契约和生命周期绑紧。能做到这三点,MVP 就算入门;做不到第三点,还不如别叫 MVP。