本文例子采用Kotlin进行编写,需要有一定的Kotlin基础
最近接触了公司的Android项目,采用的是MVP模式架构,之前倒还没用过MVP模式,这次来便是来学学
1.MVP模式优势及缺点
- View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity);
- View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试;
- Model 负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合);
- Presenter:作为View与Model交互的中间纽带,处理与用户交互的负责逻辑。
稍微形象地解释,View和Presenter这个是中间人不干活,只负责喊话,而负责干活的就是View接口和Model
当用户点击了按钮(或者是其他屏幕交互动作),触发了相关的事件操作,View就对Presenter喊话了:“现在需要进行登录操作,你去叫人。”
Presenter收到之后,会去获取相关需要的数据,之后去喊话Model:“现在需要登录操作,用户名是xx,密码是xx。”
Model接收数据处理之后,对Presenter回话:"这个密码不正确,登录失败了!"
Presenter之后拿到Model返回的数据,之后再去View接口喊话:"登录失败了,密码不正确,你把消息通过弹窗显示给用户看吧"
优势
- 降低耦合度,实现了Model和View真正的完全分离,可以修改View而不影响Modle
- 模块职责划分明显,层次清晰
- 隐藏数据
- Presenter可以复用,一个Presenter可以用于多个View,而不需要更改Presenter的逻辑(当然是在View的改动不影响业务逻辑的前提下)
- 利于测试驱动开发,由于Model中的业务逻辑与View高度分离,我们可以编写相关的单元测试来测测试我们的功能
- View可以进行组件化。在MVP当中,View不依赖Model。View只需要提供一系列接口提供给上层操作,可以实现高度可复用的View组件
- 代码灵活性
缺点
- Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。
- 由于对视图的渲染放在了Presenter中,所以视图和Presenter的交互会过于频繁。
- 如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了
- 额外的代码复杂度及学习成本
总之,凡事都有双面性,不要为了设计而设计,为了架构而架构,需要考虑项目的实际情况!
架构的最终目的是实现一个快速开发并后期维护的软件,当使用某种架构会使得项目复杂且后期维护困难,那么就不该使用此架构。
2.简单Demo实现
网上的资料我看了几篇,但发现都是顺序有点乱,这里以一个登录操作功能为例,先实现最简单的一个Demo,之后再考虑几个优化点进行代码的优化,这样阅读起来比较好,不会太突兀
功能介绍:APP的界面需要两个输入框,用来输入用户名和密码,之后一个登录按钮用来实现登录操作。当点击按钮之后,会获取用户名和密码,进行比对之后,显示“登录成功”或“登录失败”的Toast信息
2.1 定义ILoginView接口
从上面的MVP的架构图我们可以看到,有个View的接口层,其作用主要是让Presenter进行调用,简单起见,我们声明一个showToast方法,用来展示登录成功或失败的信息
interface ILoginView {
/**
* 显示Toast信息
*/
fun showToast(msg:String)
}
2.2 定义Model操作
这里,我们需要定义一个Model层,让其为Presenter提供一个登录方法
我是定义成了服务(就是相当于Model层的功能)
class UserService {
/**
* 两个相匹配返回true
*/
fun login(name:String,pwd:String):Boolean{
return name == "starsone" && pwd == "123456"
}
}
2.3 定义Presenter类
之前也是说过,Presenter是不做事的,他负责喊人,他喊的人,应该是属于他管的,所以,Presenter包含View接口和Model,也就是上面我们定义的ILoginView和UserService
class LoginPresenter{
private val loginView:ILoginView
private val userService = UserService()
constructor(iLoginView: ILoginView){
this.loginView =iLoginView
}
/**
* 登录操作,Activity中触发时间调用
*/
fun login(name: String, pwd: String) {
if (userService.login(name, pwd)) {
loginView.showToast("登录成功")
} else {
loginView.showToast("登录失败")
}
}
}
2.4 Activity调用Presenter
这里应该是各位Android开发非常熟悉的事情了,实例化一个Presenter对象,给按钮设置点击监听器,之后在点击事件调用Presenter对象的login
方法
不过,我们还得去实现View接口里的方法,让其显示Toast即可
到这一步,我们已经实现了一个简单的MVP模式demo了。调试的时候,如果输入的用户名为starsone
,密码为123456
则显示登录成功的Toast,这里就不放截图了
3.优化点
上面的简单demo其实已经实现了一个MVP模式,但由于MVP模式只是提出了某种架构,但是其代码编写并没有一个统一的标准,所以导致开发者根据自己的需要对MVP模式进行的各种魔改
所以,下面就大概说说可以优化的点,大家可以根据自己的需要和实际情况对MVP模式进行魔改。
3.1 考虑复用View接口
View接口的操作很大部分都是可以复用的,比如说弹Toast提示,显示对话框,显示加载中等等操作
所以,为了复用,我们可以构造一个通用的View接口,之后定义一个BaseActivity去实现这个View接口,之后的Activity继承这个通用的BaseActivity即可,如下代码所示:
interface IBaseView{
fun showToast(msg:String)
fun showDialog(msg:String)
fun showLoadingDialog(msg:String)
}
class BaseActivity : AppCompatActivity(),IBaseView {
override fun showToast(msg: String) {
//省略相关的代码
}
override fun showDialog(msg: String) {
//省略相关的代码
}
override fun showLoadingDialog(msg: String) {
//省略相关的代码
}
}
3.2 P层也可考虑复用
之前我们是直接定义了一个Presenter的类,考虑复用的话,我们也可以定义个Presenter的接口,之后写一个通用的BasePresenter去实现Presenter的接口,和上面的逻辑差不多,之后需要的Presenter可以继承BasePresenter来使用
3.3 考虑契约层Contact
如果View和Presenter的功能较多且都是比较单一,并不适合复用的,为了方便管理,可以考虑使用功能契约层,其中包含View接口和Presenter接口,如下代码所示:
interface ILoginContact {
interface ILoginView {
fun showToast(msg:String)
fun showDialog(msg:String)
fun showLoadingDialog(msg:String)
}
interface ILoginPresenter {
fun login(name: String, pwd: String)
}
}
使用的话,可以让Activity去实现ILoginContact.ILoginView
这个接口,定义个Presenter去实现ILoginContact.ILoginPresenter
接口,之后使用如之前所说,在事件中调用Presenter对象的相关方法即可
3.4 考虑使用弱引用
如果在Presenter需要用到View对象(某些需求需要),则需要考虑到弱引用
class LoginPresenter{
private var view:WeakREference<View>
...
}
3.5 考虑接口回调优化调用
Presenter调用Model层时,可以考虑使用接口回调的方法来优化调用
如之前我们UserService中的login方法是返回一个boolean来告知Presenter是否已经登录成功,这里我们可以改造一下
登录接口定义:
interface LoginListener{
fun loginSuccess()
fun loginFail(errorMsg:String)
}
Model改造login方法:
class UserService {
//需要传递一个LoginListener,方法不再返回boolean
fun login(name:String,pwd:String,loginListener: LoginListener){
if (name == "starsone" && pwd == "123456") {
loginListener.loginSuccess()
} else {
loginListener.loginFail("用户和密码不正确")
}
}
}
Presenter调用:
class LoginPresenter {
private val loginView: ILoginView
private val userService = UserService()
constructor(iLoginView: ILoginView) {
this.loginView = iLoginView
}
fun login(name: String, pwd: String) {
userService.login(name,pwd,object :LoginListener{
override fun loginSuccess() {
loginView.showToast("登录成功")
}
override fun loginFail(errorMsg: String) {
//由于可以回传错误信息,我们可以将其通过Toast提示用户
loginView.showToast(errorMsg)
}
})
}
}
评论区