Android 架构模式之 MVP
目录
- 架构设计的目的
- 对 MVP 的理解
- 代码
- Model
- View
- Presenter
- Android 中 MVP 的问题
- 试吃个小李子
- Model
- View
- Presenter
大家好!
作为 Android 程序猿,你有研究过 MVP 架构吗?在开始接触 Android 那一刻起,我们就开始接触 MVC 架构,可谓是用的不亦乐乎。可为什么又出现了 MVP 呢?都说它比 MVC 好,到底又好在哪里呢?
架构设计的目的
通过设计使程序模块化,模块内 高内聚、模块间 低耦合,提高开发效率,便于复用及后续维护。
对 MVP 的理解
上图是 MVP 的架构图,我们都知道,MVP架构中 M 代表 Model(模型)、V 代表 View(视图)、P 代表 Presenter(主持人/控制器)。它们的职责分别是:
- View 负责接收用户的输入事件,然后将事件传递给 Presenter;
- Presenter 收到事件后,会进行业务分发,通知 Model 获取数据;
- Model 区分数据来源,进而通过不同渠道获取数据,拿到数据后返回给 Presenter;
- Presenter 进行后续处理,或者通知 View 更新 UI。
相比 MVC 架构,MVP 架构看上去就清晰了很多:事件由 View 流向 Presenter 流向 Model,然后再由 Model 流回 Presenter 流回 View。在 MVP 架构中,Activity 就全心的处理着和 View 相关的事情,Model 负责向下分发数据请求,替 Presenter 分担了很大一部分负担,这里特意新增了一个 CacheRepository 来体现提取 Model 层的用意,这样就可以在 Model 层进行不同渠道的分发,既体现了单一职责原则,又很好的提高了代码的可读性。所以看上去是多了一层 Model 层,可实际上作用还是很大的。另外,由于 MVP 是基于接口编程,所以会多了很多接口文件,来定义业务约束,虽然看上去需要新增很多文件/函数,或者理解为额外工作量,但其实这也很好地锻炼架构思维和抽象能力,因为你可以完全放下业务实现来搭建整体框架。
代码
Model
IModel.java
public interface IModel {}
BaseModel.java
public abstract class BaseModel implements IModel {}
View
public interface IView {void showErr(String errMsg);
}
BaseActivity.java
public abstract class BaseActivity<P extends IPresenter> extends AppCompatActivityimplements IView {protected P mPresenter;public BaseActivity() {this.mPresenter = createPresenter();mPresenter.attachView(this);}public abstract P createPresenter();@Overrideprotected void onDestroy() {super.onDestroy();// Activity 销毁时,需要调用 detachView,防止内存泄漏if (mPresenter != null) {mPresenter.detachView();mPresenter.onDestroy();mPresenter = null;}}@Overridepublic void showErr(String errMsg) {Toast.makeText(this, errMsg, Toast.LENGTH_SHORT);}
}
Presenter
IPresenter.java
public interface IPresenter<V extends IView> {void attachView(V view);void detachView();void onDestroy();
}
BasePresenter.java
public abstract class BasePresenter<V extends IView, M extends IModel> implements IPresenter<V> {protected WeakReference<V> mView;protected M mModel;public BasePresenter() {this.mModel = createModel();}protected abstract M createModel();@Overridepublic void attachView(V view) {mView = new WeakReference<>(view);}@Overridepublic void detachView() {if (mView.get() != null) {mView.clear();}}@Overridepublic void onDestroy() {if (mModel != null) {mModel = null;}}
}
上述代码中可以看到,Presenter 中持有 View 引用,想象一种情况,Activity 发起一个网络请求/耗时操作,Presenter 收到需求后就分发去处理需求并等待结果了,但是还没等处理结束,Activity 就执行关闭操作了,此时 Presenter 还持有着 Activity 的强引用,导致 Activity 无法被及时回收掉,这便导致了大名鼎鼎的内存泄漏了;上述代码中通过 WeakReference 持有 View 引用,这样可以有效解决内存泄漏问题,并且在涉及到 Model/View/Presenter 的引用调用的地方,都进行了非空判断,需要规避空指针的风险出现。
Android 中 MVP 的问题
不幸的是,MVP 中 Presenter 的职责就是 MVC 中 Controller 负责的内容,只不过在 Android 中 Controller 在全职作 控制器 的同时,还需要兼职一部分 View 的职责,而 Presenter 就只是全职作 控制器。所以 Presenter 同样存在 Controller 存在的问题,随着业务的增多,Presenter 会变得越拉越 臃肿/复杂,以及 很糟糕的代码可读性。
另外,由于 MVP 模式依赖于接口,所以在新增一个业务需求时,会 爆炸式增长文件和函数,这个也很让人头疼。
其次,由于 View 会只有 Presenter 引用,Presenter 持有 View 和 Model 的引用,如果处理不当,也会存在 空指针 的风险。
最后,Presenter 会持有 View 的引用,这样就埋下了 内存泄漏 的种子,如果处理不好,问题还是蛮大的。所以就有了后来的MVVM。
试吃个小李子
点击按钮,请求 wanandroid 网站的 banner 接口数据,请求成功后更新到UI上显示接口数据
MVP 架构的 Demo 是从 MVC 架构那套代码变更过来的,添加了很多文件,主要部分 涉及上图中展开的这几个文件,仔细看上图蓝框中的内容会发现,新增一个业务 Activity,共需要新增 6 个文件,其中 3 个是接口约束类、3 个是具体实现类,这也体现了上面说的文件/函数暴增的问题。
Model
请求接口
缓存数据
IMainModel.java
public interface IMainModel extends IModel {/*** 请求 banner 数据** @param callback*/void getNetworkBanner(ResponseCallback<List<Banner>> callback);/*** 读取 banner 本地数据** @return*/List<Banner> getLocalBanner();/*** 持久化存储 banner 数据** @param banners*/void saveBanner(List<Banner> banners);/*** 清空本地数据*/void clearLocalData();
}
MainModel.java
public class MainModel extends BaseModel implements IMainModel {@Overridepublic void getNetworkBanner(ResponseCallback<List<Banner>> callback) {// 收到需求,请求接口数据NetworkRepository.getInstance().requestBanners(callback);}@Overridepublic List<Banner> getLocalBanner() {// 收到需求,读取本地数据return CacheRepository.getInstance().getBanners();}@Overridepublic void saveBanner(List<Banner> banners) {// 收到需求,持久化存储 banner 数据CacheRepository.getInstance().saveBanners(banners);}@Overridepublic void clearLocalData() {// 收到需求,清空本地缓存数据CacheRepository.getInstance().clearLocalData();}
}
View
Button1,点击请求接口数据
Button2,获取本读缓存数据
Button3,清空本地缓存数据
TextView,用于回显数据
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"tools:context=".main.MainActivity"><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="getNetworkInfo"android:text="@string/get_network_info" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="getLocalInfo"android:text="@string/get_local_info" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:onClick="clearLocalInfo"android:text="@string/clear_local_info" /><TextViewandroid:id="@+id/tv_banner_info"android:layout_width="wrap_content"android:layout_height="wrap_content" /></LinearLayout>
IMainView.java
public interface IMainView extends IView {/*** 更新 banner 数据** @param banners*/void updateBanner(List<Banner> banners, String from);
}
MainActivity.java
public class MainActivity extends BaseActivity<IMainPresenter> implements IMainView {private TextView mBannerInfoTv;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mBannerInfoTv = (TextView) findViewById(R.id.tv_banner_info);}@Overridepublic IMainPresenter createPresenter() {return new MainPresenter();}/*** 按钮点击事件** @param view*/public void getNetworkInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.getNetworkBanner();}}/*** 按钮点击事件** @param view*/public void getLocalInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.getLocalBanner();}}/*** 按钮点击事件** @param view*/public void clearLocalInfo(View view) {// 收到点击事件,交给 presenter 进行业务处理if (mPresenter != null) {mPresenter.clearLocalData();}}@Overridepublic void updateBanner(List<Banner> banners, String from) {// 收到更新 ui 事件,更新 uishowBannerInfo(banners, from);}/*** 更新UI** @param banners*/private void showBannerInfo(List<Banner> banners, String from) {StringBuilder sb = new StringBuilder();if (banners.size() > 0) {sb.append("wanandroid 官网\nhttps://www.wanandroid.com\n\n").append("data from ").append(from).append(":\n");for (Banner item : banners) {Log.e("banner", item.toString());sb.append(item.getTitle()).append('\n');}}mBannerInfoTv.setText(sb.toString());}
}
Presenter
业务处理
IMainPresenter.java
public interface IMainPresenter<V extends IView> extends IPresenter<V> {/*** 获取 banner 网络数据*/void getNetworkBanner();/*** 获取 banner 本地数据*/void getLocalBanner();/*** 存储 banner 数据** @param banners*/void saveBanner(List<Banner> banners);/*** 清空本地数据*/void clearLocalData();
}
MainPresenter.java
public class MainPresenter extends BasePresenter<IMainView, IMainModel>implements IMainPresenter<IMainView> {@Overrideprotected IMainModel createModel() {return new MainModel();}@Overridepublic void getNetworkBanner() {if (mModel == null) {return;}// 收到新需求,分发给 model 处理mModel.getNetworkBanner(new ResponseCallback<List<Banner>>() {@Overridepublic void onSuccess(List<Banner> banners) {// 数据缓存saveBanner(banners);// 通知更新UInotifyUpdateBanner(banners, CommonConstant.FROM_NETWORK);}@Overridepublic void onFail(String msg) {IMainView view = mView.get();if (view != null) {view.showErr(msg);}}});}@Overridepublic void getLocalBanner() {if (mModel == null) {return;}// 收到新需求,分发给 model 处理List<Banner> banners = mModel.getLocalBanner();// 通知更新UInotifyUpdateBanner(banners, CommonConstant.FROM_LOCAL);}@Overridepublic void saveBanner(List<Banner> banners) {// 收到新需求,分发给 model 处理if (mModel != null) {mModel.saveBanner(banners);}}@Overridepublic void clearLocalData() {// 收到新需求,分发给 model 处理if (mModel != null) {mModel.clearLocalData();}}/*** 通知更新UI** @param banners*/private void notifyUpdateBanner(List<Banner> banners, String from) {// 获取到数据,通知更新 uiif (mView != null) {IMainView view = mView.get();if (view != null) {view.updateBanner(banners, from);}}}
}
附上源码链接
致谢:
感谢 wanandroid 提供的开放API
参考:
一个小例子彻底搞懂 MVP
写在最后:
很荣幸成为一名 Android 程序猿,虽然不是一名合格的猿。一路走来磕磕绊绊,借此感谢帮助过我的人,感谢指点、感恩遇见!
相关文章:

Android 架构模式之 MVP
目录 架构设计的目的对 MVP 的理解代码ModelViewPresenter Android 中 MVP 的问题试吃个小李子ModelViewPresenter 大家好! 作为 Android 程序猿,你有研究过 MVP 架构吗?在开始接触 Android 那一刻起,我们就开始接触 MVC 架构&am…...

Ciallo~(∠・ω・ )⌒☆第二十二篇 入门request请求库使用
请求库是用于发送HTTP请求的工具。常见的请求库有requests,它是一个功能强大且易于使用的HTTP库。 使用requests库发送GET请求: import requests url "https://httpbin.org/get"# 携带get请求参数 params {"pn": 10,"size&q…...

设计模式-创建型模式-原型模式
1.原型模式定义 用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象; 1.1 原型模式优缺点 优点 当创建一个新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有的实例…...

遗传算法与深度学习实战(7)——使用遗传算法解决N皇后问题
遗传算法与深度学习实战(7)——使用遗传算法解决N皇后问题 0. 前言1. N 皇后问题2. 解的表示3. 遗传算法解决 N 皇后问题小结系列链接 0. 前言 进化算法 (Evolutionary Algorithm, EA) 和遗传算法 (Genetic Algorithms, GA) 已成功解决了许多复杂的设计…...

R语言:如何安装包“linkET”
自己在R语言中安装包“linkET”时报错不存在叫‘linket’这个名字的程辑包 尝试了install.packages("linkET")和BiocManager::install("linkET")两种安装办法都不行 >install.packages("linkET") WARNING: Rtools is required to build R pa…...

JSON, YAML, XML, CSV交互可视化
1、jsoncrack https://jsoncrack.com/editor...
Android UI:PopupWindow:源码分析:设置WindowManager.LayoutParams中的各种参数
文章目录 设置flags是否包含某些flag设置gravity设置type设置softInputMode设置windowAnimations设置width/height设置token 在WindowManager.addView之前设置在WindowManager.addView之后,可通过i熬夜难过update方法设置设置format设置flags是否包含某些flag 1666 …...
MySQL:从入门到放弃
基础查询 MySQL:基础查询 Mybatis:基础巩固-DDL 项目实战 MySQL:按照日期分组查询 查询开始时间与结束时间在指定的日期范围之内,并且结束时间可以为NULL的数据...

C++OpenGL三维显示镜面反射光线漫反射实例
程序示例精选 COpenGL三维显示镜面反射光线漫反射实例 如需安装运行环境或远程调试,见文章底部个人QQ名片,由专业技术人员远程协助! 前言 这篇博客针对《COpenGL三维显示镜面反射光线漫反射实例》编写代码,代码整洁,…...
【前端面试】从npm 升级到 pnpm的总结
pnpm优势 pnpm 和 npm 在性能上存在一些明显的差异,这也是一些开发者选择从 npm 切换到 pnpm 的原因。以下是一些关键的差异和原因: 1. 速度: pnpm 比 npm 快了近 2 倍,它通过优化的依赖管理,显著提高了安装速度 。 2. 磁盘空间效率: pnpm 使用基于内容寻址的文件系…...
同步外网YUM源-3
在企业实际应用场景中,仅仅靠光盘里面的RPM软件包是不能满足需要,我们可以把外网的YUM源中的所有软件包同步至本地,可以完善本地YUM源的软件包数量及完整性。 获取外网YUM源软件常见方法包括Rsync、Wget、Reposync,三种同步方法的区别Rsync方式需要外网YUM源支持RSYNC协议…...

Linux的oracle数据库导入其他用户导出的数据库文件
如果用户使用的是expdp的命令,导入就要使用impdp命令,本文以impdp为例进行介绍 1、查看当前创建的所有dmp导出目录 select * from dba_directories 2、为创建的目录赋权限 比如咱们将数据库导入到test用户, grant read,write on directo…...

FLUX.1 文生图模型微调指南
FLUX.1 是 Black Forest Labs 今年夏天发布的文本转图像模型系列。FLUX.1 模型为开源图像生成模型树立了新标准:它们可以生成逼真的手、清晰的文本,甚至可以生成搞笑表情包这样异常困难的任务。 现在,你可以使用 Ostris 的 Replicate 上的 A…...
JavaWeb基础:HTTP协议与Tomcat服务器
目录 1. HTTP协议简介 示例代码:创建HTTP GET请求 2. Tomcat服务器介绍 Tomcat的基本操作 示例代码:部署简单Servlet 3. 使用Servlet处理请求 示例代码:处理POST请求 在现代网络开发中,理解HTTP协议和如何使用Tomcat作为服…...

python井字棋游戏设计与实现
python实现井字棋游戏 游戏规则,有三个井字棋盘,看谁连成的直线棋盘多谁就获胜 棋盘的展现形式为 棋盘号ABC和位置数字1-9 输入A1 代表在A棋盘1号位数下棋 效果图如下 部分源码如下: 卫星工纵浩 白龙码程序设计,点 代码获取 …...

据说是可以和 Windows 一拼的 5个 Linux 发行版
现如今有数以千计的 Linux 发行版可供您使用,然而人们却无法选择一个完美的操作系统来替代 Windows。 使用 Windows 时,傻瓜都能操作自如,同样的方法却不适用于 Linux。在这里,您必须具备操作和使用操作系统的基本知识。因此人们经…...
PHP 常用函数
1. ksort() 如果你有一个数组 array([11] > array(XX), [6] > array(YYY)),你想要返回按照key重新排序,并不改变键和值之间的关联,处理之后的结果为 array([6] > array(YY…...

如何将MySQL迁移到TiDB,完成无缝业务切换?
当 MySQL 数据库的单表数据量达到了亿级,会发生什么? 这个现象表示公司的业务上了一个台阶,随着数据量的增加,公司规模也进一步扩大了,是非常喜人的一个改变 ,然而随之而来的其他变化,就没那么…...

【嵌入式烧录刷写文件】-2.10-为一个Intel Hex文件计算校验和Checksum
案例背景(共6页精讲): 有如下一段Intel Hex文件,为其创建Checksum校验和:CRC16,CRC32(CVN),SHA-256 Hash算法…, 将Checksum Value填充到指定地址。 :2091000058595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767…...

整体思想以及取模
前言:一开始由于失误,误以为分数相加取模不能,但是其实是可以取模的 这个题目如果按照一般方法,到达每个节点再进行概率统计,但是不知道为什么只过了百分之十五的测试集 题目地址 附上没过关的代码 #include<bits…...

SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...

2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...
Kubernetes 网络模型深度解析:Pod IP 与 Service 的负载均衡机制,Service到底是什么?
Pod IP 的本质与特性 Pod IP 的定位 纯端点地址:Pod IP 是分配给 Pod 网络命名空间的真实 IP 地址(如 10.244.1.2)无特殊名称:在 Kubernetes 中,它通常被称为 “Pod IP” 或 “容器 IP”生命周期:与 Pod …...