Android 架构模式之 MVP

news2024/12/24 11:33:39

目录

  • 架构设计的目的
  • 对 MVP 的理解
  • 代码
    • Model
    • View
    • Presenter
  • Android 中 MVP 的问题
  • 试吃个小李子
    • Model
    • View
    • Presenter

大家好!

作为 Android 程序猿,你有研究过 MVP 架构吗?在开始接触 Android 那一刻起,我们就开始接触 MVC 架构,可谓是用的不亦乐乎。可为什么又出现了 MVP 呢?都说它比 MVC 好,到底又好在哪里呢?

架构设计的目的

通过设计使程序模块化,模块内 高内聚、模块间 低耦合,提高开发效率,便于复用及后续维护。

对 MVP 的理解

MVP 架构图,箭头代表事件流向

上图是 MVP 的架构图,我们都知道,MVP架构中 M 代表 Model(模型)、V 代表 View(视图)、P 代表 Presenter(主持人/控制器)。它们的职责分别是:

  1. View 负责接收用户的输入事件,然后将事件传递给 Presenter;
  2. Presenter 收到事件后,会进行业务处理,通知 Model 获取数据;
  3. Model 根据获取数据方式通过不同渠道去取数据,拿到数据后返回给 Presenter;
  4. Presenter 进行后续处理,或者通知 View 更新 UI。

相比 MVC 架构,MVP 架构看上去就清晰了很多:事件由 View 流向 Presenter 流向 Model,然后再由 Model 流回 Presenter 流回 View。在 MVP 架构中,Activity 就全心的处理着和 View 相关的事情,Model 负责向下分发数据请求,替 Presenter 分担了很大一部分负担,这里特意新增了一个 CacheRepository 来体现提取 Model 层的用意,这样就可以在 Model 层进行不同渠道的分发,既体现了单一职责原则,又很好的提高了代码的可读性。所以看上去是多了一层 Model 层,可实际上作用还是很大的。

代码

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 AppCompatActivity
        implements IView {

    protected P mPresenter;

    public BaseActivity() {
        this.mPresenter = createPresenter();
        mPresenter.attachView(this);
    }

    public abstract P createPresenter();

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // Activity 销毁时,需要调用 detachView,防止内存泄漏
        if (mPresenter != null) {
            mPresenter.detachView();
            mPresenter.onDestroy();
            mPresenter = null;
        }
    }

    @Override
    public 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();

    @Override
    public void attachView(V view) {
        mView = new WeakReference<>(view);
    }

    @Override
    public void detachView() {
        if (mView.get() != null) {
            mView.clear();
        }
    }

    @Override
    public 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 {
    @Override
    public void getNetworkBanner(ResponseCallback<List<Banner>> callback) {
        // 收到需求,请求接口数据
        NetworkRepository.getInstance().requestBanners(callback);
    }

    @Override
    public List<Banner> getLocalBanner() {
        // 收到需求,读取本地数据
        return CacheRepository.getInstance().getBanners();
    }

    @Override
    public void saveBanner(List<Banner> banners) {
        // 收到需求,持久化存储 banner 数据
        CacheRepository.getInstance().saveBanners(banners);
    }

    @Override
    public 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">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="getNetworkInfo"
        android:text="@string/get_network_info" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="getLocalInfo"
        android:text="@string/get_local_info" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="clearLocalInfo"
        android:text="@string/clear_local_info" />

    <TextView
        android: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;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mBannerInfoTv = (TextView) findViewById(R.id.tv_banner_info);
    }

    @Override
    public 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();
        }
    }

    @Override
    public void updateBanner(List<Banner> banners, String from) {
        // 收到更新 ui 事件,更新 ui
        showBannerInfo(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> {

    @Override
    protected IMainModel createModel() {
        return new MainModel();
    }

    @Override
    public void getNetworkBanner() {
        if (mModel == null) {
            return;
        }
        // 收到新需求,分发给 model 处理
        mModel.getNetworkBanner(new ResponseCallback<List<Banner>>() {
            @Override
            public void onSuccess(List<Banner> banners) {
                // 数据缓存
                saveBanner(banners);

                // 通知更新UI
                notifyUpdateBanner(banners, CommonConstant.FROM_NETWORK);
            }

            @Override
            public void onFail(String msg) {
                IMainView view = mView.get();
                if (view != null) {
                    view.showErr(msg);
                }
            }
        });
    }

    @Override
    public void getLocalBanner() {
        if (mModel == null) {
            return;
        }
        // 收到新需求,分发给 model 处理
        List<Banner> banners = mModel.getLocalBanner();

        // 通知更新UI
        notifyUpdateBanner(banners, CommonConstant.FROM_LOCAL);
    }

    @Override
    public void saveBanner(List<Banner> banners) {
        // 收到新需求,分发给 model 处理
        if (mModel != null) {
            mModel.saveBanner(banners);
        }
    }

    @Override
    public void clearLocalData() {
        // 收到新需求,分发给 model 处理
        if (mModel != null) {
            mModel.clearLocalData();
        }
    }

    /**
     * 通知更新UI
     *
     * @param banners
     */
    private void notifyUpdateBanner(List<Banner> banners, String from) {
        // 获取到数据,通知更新 ui
        if (mView != null) {
            IMainView view = mView.get();
            if (view != null) {
                view.updateBanner(banners, from);
            }
        }
    }
}

附上源码链接

致谢:
感谢 wanandroid 提供的开放API

参考:
一个小例子彻底搞懂 MVP

写在最后:
很荣幸成为一名 Android 程序猿,虽然不是一名合格的猿。一路走来磕磕绊绊,借此感谢帮助过我的人,感谢指点、感恩遇见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2054273.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

“解决Windows电脑无法投影到其他屏幕的问题:尝试更新驱动程序或更换视频卡“

背景: 今天在日常的工作中&#xff0c; 我想将笔记本分屏到另一个显示屏&#xff0c;我这电脑Windows10&#xff0c;当我按下Windows键P键&#xff0c;提示我"你的电脑不能投影到其他屏幕&#xff0c;请尝试从新安装驱动程序或使用"遇到这种问题。 解决方法1: 1.快…

解决 idea 创建maven项目卡住

一, 现象 选择一个Archetype后创建项目,一直卡着,点哪里都点不了,有的博客说可以看maven的日志排查问题,我这里没有任何日志输出 二,为什么会卡住 结论: 因为idea在从中央仓库下载archetype-catalog.xml(文件较大,14.8M)导致卡住 分析: 首先要明白通过Archetype创建…

openssl查看证书公钥 openssl 验证证书和密钥

例如&#xff1a;中间件或者openssl生成国密证书请求文件文件里面省份必须写陕西省三个汉字 安装完成后&#xff0c;使用下列命令查看该版本的openssl是否支持SM2参数&#xff1a; openssl ecparam -list_curves | grep SM2 查看openssl版本信息 openssl version -a 查看open…

【C++篇】迈入新世界的大门——初识C++(下篇)

文章目录 前言引用引用的概念和定义引用的特性引用的使用const引用指针和引用的关系 inline#define定义宏inline nullptr 前言 接上篇&#xff1a;【C篇】迈入新世界的大门——初识C&#xff08;上篇&#xff09; 引用 引用的概念和定义 引⽤不是新定义⼀个变量&#xff0c;…

第10章 无持久存储的文件系统 (3)

目录 10.2 简单文件系统 10.2.1 顺序文件 10.2.2 用libfs编写文件系统 10.2.3 调试文件系统 10.2.4 伪文件系统 10.3 sysfs 10.3.1 概述 10.3.2 数据结构 10.3.3 装载文件系统 10.3.4 文件和目录操作 10.3.5 向sysfs添加内容 10.4 小结 本专栏文章将有70篇左右&…

Node.js及mysql的安装,建立页面,javascript对mySQL数据库的操作过程

具体动态效果看视频 node.js连接MySQL数据库操作 第一部分&#xff1b;配置服务器环境 Nods.js, NPM,CNPM,mysql2,express的安装 前往 Node.js 官方网站&#xff08;https://nodejs.org/&#xff09;下载并安装最新的稳定版本&#xff0c;确定配置好path环境变量&#xff0c;其…

Linux网络环境搭建,开发板网线直连电脑网口,电脑WIFI上网

开发板网线直连电脑网口&#xff08;电脑自带&#xff0c;一般有PCI&#xff0c;不是USB网卡&#xff09;&#xff0c;电脑WIFI上网 因为电脑是 WiFi 上网&#xff0c;所以需要添加一个网络适配器并设置成 NAT 模式&#xff0c;供虚拟机上网。 设置双网卡&#xff0c;注意双网卡…

SQL 时间盲注 (injection 第十五关)

简介 SQL注入&#xff08;SQL Injection&#xff09;是一种常见的网络攻击方式&#xff0c;通过向SQL查询中插入恶意的SQL代码&#xff0c;攻击者可以操控数据库&#xff0c;SQL注入是一种代码注入攻击&#xff0c;其中攻击者将恶意的SQL代码插入到应用程序的输入字段中&#x…

visual studio使用技巧:快速生成Json、XML对应类

visual studio快速生成Json、XML对应类 在项目中经常用到json或者xml作为配置文件&#xff0c;进行序列化和反序列化就需要有对应的类&#xff0c;重新写一遍类就比较麻烦&#xff0c;这里就讲一下通过visual studio快速生成json或者xml对应类型的方法。 自动生成Json类 复制…

大数据-90 Spark 集群 RDD 编程-高阶 RDD容错机制、RDD的分区、自定义分区器(Scala编写)、RDD创建方式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

【Python】AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘

【Python】成功解决AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘ 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博…

MySQL集群+Keepalived实现高可用部署

Mysql高可用集群-双主双活-myqlkeeplived 一、特殊情况 常见案例&#xff1a;当生产环境中&#xff0c;当应用服务使用了mysql-1连接信息&#xff0c;在升级打包过程中或者有高频的数据持续写入【对数据一致性要求比较高的场景】&#xff0c;这种情况下&#xff0c;数据库连接…

STM32之继电器与震动传感器的使用,实现震动灯

在STM32的外设应用中&#xff0c;继电器扮演着重要的角色。继电器作为一种电控制器件&#xff0c;其主要作用是通过小电流控制大电流的通断&#xff0c;实现电路的自动控制和保护。具体来说&#xff0c;继电器在STM32外设中的作用可以归纳为以下几点&#xff1a; 电路隔离与保…

在线学习考试设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

“CSS”第一步——WEB开发系列13

CSS (Cascading Style Sheets&#xff0c;层叠样式表&#xff09;&#xff0c;是一种用来为结构化文档&#xff08;如 HTML 文档或 XML 应用&#xff09;添加样式&#xff08;字体、间距和颜色等&#xff09;的计算机语言&#xff0c;CSS 文件扩展名为 .css。 一、什么是 CSS&a…

ubuntu x86_64系统上安装运行aarch系统的虚拟机

安装qemu-system-aarch64 创建sda.qcow2 虚拟磁盘 运行命令启动虚拟机 sudo qemu-system-aarch64 -M virt-4.0 -m 4G -cpu cortex-a57 -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd -cdrom ~/下载/openEuler-24.03-LTS-aarch64-dvd.iso -drive ifnone,filesda.qcow2,idhd0…

王老师 linux c++ 通信架构 笔记(五)编译后生成的 nginx 可执行程序的启动

&#xff08;22&#xff09; 启动 nginx &#xff1a; 上网测试一下&#xff1a; 端口号 介绍&#xff1a; &#xff08;23&#xff09; 因为 nginx 监听知名端口号 80 &#xff0c;http 服务。也可以知名端口号&#xff0c;格式如下&#xff1a; 生产环境下可以设置 ngi…

Pulsar官方文档学习笔记——架构概览

架构概览 在最高配置下&#xff0c;pulsar服务应该由一个或多个pulsar集群组成。 一个pulsar集群可以包括如下组件 一个或多个broker。broker会将生产者 的消息分派给消费者。与pulsar配置存储通信来协调各种任务。将消息 存储在 BookKeeper实例中 &#xff08;也可以叫book…

计算机毕业设计选什么题目好?springboot 基于Java的学院教学工作量统计系统

✍✍计算机毕业编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java、…

java生成随机数字,生成随机ID

java在代码中生成随机数字和ID的两个方法 import java.util.UUID; import java.util.Random; public class randomID {public static void main(String[] args) {// TODO Auto-generated method stubUUID uuid UUID.randomUUID();String randomId uuid.toString();System.ou…