[Android] MVP设计模式及实例

MVP简介

MVP 所对应的意义:M-Model-模型、V-View-视图、P-Presenter-主持人。

MVP 的结构图如下所示,对于这个图理解即可而不必局限其中的条条框框,毕竟在不同的场景下多少会有些出入的。 Presenter 与 View 、 Model 的交互使用接口可以进一步达到松耦合。

Model

Model 是处理图形界面所需要数据的地方,大多数的数据存取逻辑都会在此处进行。

View

视图这一层体现的很轻薄,负责显示数据、提供友好界面跟用户交互就行。MVP 下 Activity 和 Fragment 体现在了这一层,Activity 一般也就做加载 UI 视图、设置监听再交由 Presenter 处理的一些工作,所以也就需要持有相应 Presenter 的引用。在 View 上输入的数据做一些判断时,例如, EditText 的输入数据,假如是简单的非空判断则可以作为 View 层的逻辑,而当需要对 EditText 的数据进行更复杂的比较时,如从数据库获取本地数据进行判断时明显需要经过 Model 层才能返回了,所以这些细节需要自己掂量。

Presenter

Presenter这一层处理着程序各种逻辑的分发,收到View层UI上的反馈命令、定时命令、系统命令等指令后分发处理逻辑交由业务层做具体的业务操作,然后将得到的 Model 给 View 显示。

特点

总的来说 , 就是 View 和 Model 之间完全隔离 , 只通过中间的 Presenter 来交换信息 , 中间的 Presenter 接收 View 发过来的请求 , 然后通知 Model 执行数据存取 , 完成后 Model 将数据提交给 Presenter , 并由 Presenter 来通知 View 更新 . 其中基本都试用接口来进行交互 . 在需要更换业务逻辑的时候 , 可以保留之前的逻辑, 实现新的即可 , 也便于测试 .

缺点

使用了 MVP 后 , 接口的数量会暴涨 , 代码量也相对增加 , 对于小型项目个人觉得没必要使用 .

实例

下面的实例可以在我的 Github 上得到 :github
下面通过一个 IP 地址归属地查询的小 Demo 来演示 MVP 的大致结构吧

结果.jpg

先来看看项目结构

项目结构.jpg

bean 的类没啥可说的 , 可以看到三个角色都有接口 , 和接口实现类
其中 SearchActivity 是 ISearchView 的实现类 .

先看看 MyIP 把:

public class MyIP {

  String City;
  String Country;
  String Province;

  public String getCity() {
    return City;
  }

  public void setCity(String city) {
    City = city;
  }

  public String getCountry() {
    return Country;
  }

  public void setCountry(String country) {
    Country = country;
  }

  public String getProvince() {
    return Province;
  }

  public void setProvince(String province) {
    Province = province;
  }

  @Override public String toString() {

    return this.getCountry() + "\n" +
        this.getProvince() + "\n" +
        this.getCity() + "\n";
  }
}

定义了一些 get/set 方法 , 和重写了 toString 方法.

接下来我们从 View 开始来看吧 毕竟业务的流程是从 View 开始的.

public interface ISearchView {
  String getIPaddress();

  void setMsg(MyIP myIP);

  void hideLoad();

  void showLoad();
}

接口定义了四个方法 , 分别是获取 IP 地址 , 设置显示 IP 信息 , 隐藏加载提示和显示加载提示 .

接下来在 SearchActivity 中实现他们

public class SearchActivity extends AppCompatActivity implements ISearchView {
  private EditText et_ip;
  private Button btn_search;
  private TextView tv_msg;
  private ProgressBar pb_load;
  private SearchPresenter mSearchPresenter = new SearchPresenter(this);

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

    et_ip = (EditText) findViewById(R.id.et_ip);
    btn_search = (Button) findViewById(R.id.btn_search);
    tv_msg = (TextView) findViewById(R.id.tv_msg);
    pb_load = (ProgressBar) findViewById(R.id.pb_load);

    onClick();
  }

  private void onClick() {
    btn_search.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        mSearchPresenter.Search();
      }
    });
  }

  @Override public String getIPaddress() {
    return et_ip.getText().toString().trim();
  }

  @Override public void setMsg(MyIP myIP) {
    if (myIP != null) {
      tv_msg.setText(myIP.toString());
    } else {
      tv_msg.setText("获取失败");
    }
  }

  @Override public void hideLoad() {
    pb_load.setVisibility(View.GONE);
  }

  @Override public void showLoad() {
    pb_load.setVisibility(View.VISIBLE);
  }
}

我们先看上面的四个接口是如何实现的

getIPaddress 从文本框取出了文本信息并返回 .

setMsg 通过调用 MyIP 的 toString 得到 IP归属地信息并设置到界面上

hideLoad 和 showLoad 是控制加载圆圈显示和隐藏的

这四个方法都是对接口的实现 , 并且都是视图相关的功能 , 后面可以通过调用接口来触发这些方法 .

在上面有一个

private SearchPresenter mSearchPresenter = new SearchPresenter(this);

这是实例化了 SearchPresenter 的实现类 , 用来向 Presenter 提交任务 , 下面的点击事件中有一个

mSearchPresenter.Search();

就是向 Presenter 提交了一个搜索 IP 的任务 .

接下来看看 ISearchPresenter 吧

public interface ISearchPresenter {  void  Search();}

这个接口中只有一个方法 , 就是搜索 , 这是根据当前的业务需求定义的 . 还有一个是 onSearchListener

public interface onSearchListener {

  void onSuccess(MyIP myIP);

  void onError();
}

这是用于在 Model 中处理数据完成时候的回调接口 . 下面会用到 .

再看看 SearchPresenter 也就是 SearchPresenter 的实现类

public class SearchPresenter implements ISearchPresenter,onSearchListener {

  ISearchView searchView;
  ISearchModel searchModel;
  Handler handler;

  public SearchPresenter(ISearchView searchView) {
    this.searchView = searchView;
    searchModel = new SearchModel();
    handler = new Handler(Looper.getMainLooper());
  }

  @Override public void Search() {
    searchView.showLoad();
    searchModel.getIPaddressInfo(searchView.getIPaddress(),this);
  }

  @Override public void onSuccess(final MyIP myIP) {
    handler.post(new Runnable() {
      @Override public void run() {
        searchView.setMsg(myIP);
        searchView.hideLoad();
      }
    });
  }

  @Override public void onError() {
    handler.post(new Runnable() {
      @Override public void run() {
        searchView.setMsg(null);
        searchView.hideLoad();
      }
    });
  }  
}

这个类中在构造方法处拿到了 View 和 Model 的接口对象 , 以此来协调两边的操作 , 并且拿到一个主线程相关的 Handler 用于网络操作结束后更新 UI .

其中的 Search 方法先是调用 View 接口的 showLoad 方法显示加载圆圈 , 然后通知 Model 获取 IP 信息 , 并将从 View 接口得到的 IP 地址和自己(这里是指实现了 onSearchListener 的部分)传递给 Model .

下面的 onSuccess 和 onError 一会再说 .

我们来看 ISearchModel Model模块的接口类

public interface ISearchModel {

  void getIPaddressInfo(String ipAddress,onSearchListener onSearchListener);
}

这里只有一个方法 , 就是获取 IP 相关信息 , 需要 IP 地址和一个回调接口 . 下面看看具体是怎么实现的 .

public class SearchModel implements ISearchModel {

  @Override public void getIPaddressInfo(final String ipAddress, final onSearchListener onSearchListener) {

    new Thread(new Runnable() {
      @Override public void run() {
        String apiUrl = "http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=Json&ip=";
        try {
          MyIP myIP = new MyIP();
          URL url = new URL(apiUrl + ipAddress);
          HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();

          httpURLConnection.setRequestMethod("GET");

          InputStreamReader isp = new InputStreamReader(httpURLConnection.getInputStream());
          BufferedReader bf = new BufferedReader(isp);
          String line;
          StringBuilder stringBuilder = new StringBuilder();
          while ((line = bf.readLine()) != null) {
            stringBuilder.append(line).append("\n");
          }

          JSONObject jsonObject = new JSONObject(String.valueOf(stringBuilder));

          myIP.setCountry(jsonObject.getString("country"));
          myIP.setProvince(jsonObject.getString("province"));
          myIP.setCity(jsonObject.getString("city"));
          onSearchListener.onSuccess(myIP);
        } catch (IOException | JSONException e) {
          onSearchListener.onError();
        }
      }
    }).start();

  }
}

在一个子线程中开启网络连接 , 调用 API 并得到返回的 JSON 数据 , 将 JSON 中的数据提取出来 , 实例化为一个 MyIP 对象 , 在成功的时候调用 onSearchListener.onSuccess(myIP); 方法把结果对象传进去 . 在失败的时候调用 onSearchListener.onError(); 让接口的实现类来处理.

我们跟着流程 , 现在 Model 得到了数据 , 通过回调接口调用了 onSuccess , 那就看看它把.

@Override public void onSuccess(final MyIP myIP) {
   handler.post(new Runnable() {
     @Override public void run() {
       searchView.setMsg(myIP);
       searchView.hideLoad();
     }
   });
 }

onSuccess 是在 Presenter 中的 , 他在 Model 中的子线程被调用 , 所以这里需要在 主线程中调用他 , 所以用到了上面的 handler . 这里使用了 searchView.setMsg(myIP); 来通知 View 部分更新界面 , 并且把相关数据传递给 View . 最后隐藏加载圆圈 .

至此 , 整个操作流程就算是完成了 . 我们来总结一下过程 .

  1. View 收到用户指令(点击事件) , 通知 Persenter 进行处理 , 并将参数传递给 Persenter .

  2. Persenter 收到通知后 , 调用 Search 方法通知 Model 进行处理 , 并将参数和回调接口传递给 Model , 还通知了 View 显示加载圆圈 .

  3. Model 接着就被触发了 getIPaddressInfo 方法 , 开启了子线程从网络中获取数据 . 并在获取后通过回调接口返回了数据 .

  4. Persenter 中的回调接口被 Model 触发后 , 将从 Model 得到的数据传递给 View 并且通知 View 进行界面的更新 .

我们可以看到 , 数据流是单向流动的 . 中间的 Presenter 担当了桥梁的角色 , 协调两边进行处理 . MVP 的一个好处就是低耦合 , 三个部分都通过接口来互动 , 当我们修改的时候 , 只需要修改或添加实现类即可 .

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容