[Android] MVP設(shè)計模式及實例

MVP簡介

MVP 所對應的意義:M-Model-模型、V-View-視圖、P-Presenter-主持人纳猪。

MVP 的結(jié)構(gòu)圖如下所示淀散,對于這個圖理解即可而不必局限其中的條條框框右莱,畢竟在不同的場景下多少會有些出入的。 Presenter 與 View 档插、 Model 的交互使用接口可以進一步達到松耦合慢蜓。

Model

Model 是處理圖形界面所需要數(shù)據(jù)的地方,大多數(shù)的數(shù)據(jù)存取邏輯都會在此處進行。

View

視圖這一層體現(xiàn)的很輕薄郭膛,負責顯示數(shù)據(jù)晨抡、提供友好界面跟用戶交互就行。MVP 下 Activity 和 Fragment 體現(xiàn)在了這一層则剃,Activity 一般也就做加載 UI 視圖抱冷、設(shè)置監(jiān)聽再交由 Presenter 處理的一些工作,所以也就需要持有相應 Presenter 的引用虎囚。在 View 上輸入的數(shù)據(jù)做一些判斷時邮丰,例如, EditText 的輸入數(shù)據(jù)轴咱,假如是簡單的非空判斷則可以作為 View 層的邏輯汛蝙,而當需要對 EditText 的數(shù)據(jù)進行更復雜的比較時,如從數(shù)據(jù)庫獲取本地數(shù)據(jù)進行判斷時明顯需要經(jīng)過 Model 層才能返回了朴肺,所以這些細節(jié)需要自己掂量窖剑。

Presenter

Presenter這一層處理著程序各種邏輯的分發(fā),收到View層UI上的反饋命令戈稿、定時命令西土、系統(tǒng)命令等指令后分發(fā)處理邏輯交由業(yè)務(wù)層做具體的業(yè)務(wù)操作,然后將得到的 Model 給 View 顯示鞍盗。

特點

總的來說 , 就是 View 和 Model 之間完全隔離 , 只通過中間的 Presenter 來交換信息 , 中間的 Presenter 接收 View 發(fā)過來的請求 , 然后通知 Model 執(zhí)行數(shù)據(jù)存取 , 完成后 Model 將數(shù)據(jù)提交給 Presenter , 并由 Presenter 來通知 View 更新 . 其中基本都試用接口來進行交互 . 在需要更換業(yè)務(wù)邏輯的時候 , 可以保留之前的邏輯, 實現(xiàn)新的即可 , 也便于測試 .

缺點

使用了 MVP 后 , 接口的數(shù)量會暴漲 , 代碼量也相對增加 , 對于小型項目個人覺得沒必要使用 .

實例

下面的實例可以在我的 Github 上得到 :github
下面通過一個 IP 地址歸屬地查詢的小 Demo 來演示 MVP 的大致結(jié)構(gòu)吧

結(jié)果.jpg

先來看看項目結(jié)構(gòu)

項目結(jié)構(gòu).jpg

bean 的類沒啥可說的 , 可以看到三個角色都有接口 , 和接口實現(xiàn)類
其中 SearchActivity 是 ISearchView 的實現(xiàn)類 .

先看看 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 開始來看吧 畢竟業(yè)務(wù)的流程是從 View 開始的.

public interface ISearchView {
  String getIPaddress();

  void setMsg(MyIP myIP);

  void hideLoad();

  void showLoad();
}

接口定義了四個方法 , 分別是獲取 IP 地址 , 設(shè)置顯示 IP 信息 , 隱藏加載提示和顯示加載提示 .

接下來在 SearchActivity 中實現(xiàn)他們

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);
  }
}

我們先看上面的四個接口是如何實現(xiàn)的

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

setMsg 通過調(diào)用 MyIP 的 toString 得到 IP歸屬地信息并設(shè)置到界面上

hideLoad 和 showLoad 是控制加載圓圈顯示和隱藏的

這四個方法都是對接口的實現(xiàn) , 并且都是視圖相關(guān)的功能 , 后面可以通過調(diào)用接口來觸發(fā)這些方法 .

在上面有一個

private SearchPresenter mSearchPresenter = new SearchPresenter(this);

這是實例化了 SearchPresenter 的實現(xiàn)類 , 用來向 Presenter 提交任務(wù) , 下面的點擊事件中有一個

mSearchPresenter.Search();

就是向 Presenter 提交了一個搜索 IP 的任務(wù) .

接下來看看 ISearchPresenter 吧

public interface ISearchPresenter {  void  Search();}

這個接口中只有一個方法 , 就是搜索 , 這是根據(jù)當前的業(yè)務(wù)需求定義的 . 還有一個是 onSearchListener

public interface onSearchListener {

  void onSuccess(MyIP myIP);

  void onError();
}

這是用于在 Model 中處理數(shù)據(jù)完成時候的回調(diào)接口 . 下面會用到 .

再看看 SearchPresenter 也就是 SearchPresenter 的實現(xiàn)類

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

這個類中在構(gòu)造方法處拿到了 View 和 Model 的接口對象 , 以此來協(xié)調(diào)兩邊的操作 , 并且拿到一個主線程相關(guān)的 Handler 用于網(wǎng)絡(luò)操作結(jié)束后更新 UI .

其中的 Search 方法先是調(diào)用 View 接口的 showLoad 方法顯示加載圓圈 , 然后通知 Model 獲取 IP 信息 , 并將從 View 接口得到的 IP 地址和自己(這里是指實現(xiàn)了 onSearchListener 的部分)傳遞給 Model .

下面的 onSuccess 和 onError 一會再說 .

我們來看 ISearchModel Model模塊的接口類

public interface ISearchModel {

  void getIPaddressInfo(String ipAddress,onSearchListener onSearchListener);
}

這里只有一個方法 , 就是獲取 IP 相關(guān)信息 , 需要 IP 地址和一個回調(diào)接口 . 下面看看具體是怎么實現(xiàn)的 .

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();

  }
}

在一個子線程中開啟網(wǎng)絡(luò)連接 , 調(diào)用 API 并得到返回的 JSON 數(shù)據(jù) , 將 JSON 中的數(shù)據(jù)提取出來 , 實例化為一個 MyIP 對象 , 在成功的時候調(diào)用 onSearchListener.onSuccess(myIP); 方法把結(jié)果對象傳進去 . 在失敗的時候調(diào)用 onSearchListener.onError(); 讓接口的實現(xiàn)類來處理.

我們跟著流程 , 現(xiàn)在 Model 得到了數(shù)據(jù) , 通過回調(diào)接口調(diào)用了 onSuccess , 那就看看它把.

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

onSuccess 是在 Presenter 中的 , 他在 Model 中的子線程被調(diào)用 , 所以這里需要在 主線程中調(diào)用他 , 所以用到了上面的 handler . 這里使用了 searchView.setMsg(myIP); 來通知 View 部分更新界面 , 并且把相關(guān)數(shù)據(jù)傳遞給 View . 最后隱藏加載圓圈 .

至此 , 整個操作流程就算是完成了 . 我們來總結(jié)一下過程 .

  1. View 收到用戶指令(點擊事件) , 通知 Persenter 進行處理 , 并將參數(shù)傳遞給 Persenter .

  2. Persenter 收到通知后 , 調(diào)用 Search 方法通知 Model 進行處理 , 并將參數(shù)和回調(diào)接口傳遞給 Model , 還通知了 View 顯示加載圓圈 .

  3. Model 接著就被觸發(fā)了 getIPaddressInfo 方法 , 開啟了子線程從網(wǎng)絡(luò)中獲取數(shù)據(jù) . 并在獲取后通過回調(diào)接口返回了數(shù)據(jù) .

  4. Persenter 中的回調(diào)接口被 Model 觸發(fā)后 , 將從 Model 得到的數(shù)據(jù)傳遞給 View 并且通知 View 進行界面的更新 .

我們可以看到 , 數(shù)據(jù)流是單向流動的 . 中間的 Presenter 擔當了橋梁的角色 , 協(xié)調(diào)兩邊進行處理 . MVP 的一個好處就是低耦合 , 三個部分都通過接口來互動 , 當我們修改的時候 , 只需要修改或添加實現(xiàn)類即可 .

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末需了,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子般甲,更是在濱河造成了極大的恐慌肋乍,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件敷存,死亡現(xiàn)場離奇詭異墓造,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門觅闽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帝雇,“玉大人,你說我怎么就攤上這事蛉拙∈ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵孕锄,是天一觀的道長室叉。 經(jīng)常有香客問我,道長硫惕,這世上最難降的妖魔是什么茧痕? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮恼除,結(jié)果婚禮上踪旷,老公的妹妹穿的比我還像新娘。我一直安慰自己豁辉,他們只是感情好令野,可當我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著徽级,像睡著了一般气破。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上餐抢,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天现使,我揣著相機與錄音,去河邊找鬼旷痕。 笑死碳锈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的欺抗。 我是一名探鬼主播售碳,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼绞呈!你這毒婦竟也來了贸人?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤佃声,失蹤者是張志新(化名)和其女友劉穎艺智,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秉溉,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡力惯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年碗誉,在試婚紗的時候發(fā)現(xiàn)自己被綠了召嘶。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片父晶。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖弄跌,靈堂內(nèi)的尸體忽然破棺而出甲喝,到底是詐尸還是另有隱情,我是刑警寧澤铛只,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布埠胖,位于F島的核電站,受9級特大地震影響淳玩,放射性物質(zhì)發(fā)生泄漏直撤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一蜕着、第九天 我趴在偏房一處隱蔽的房頂上張望谋竖。 院中可真熱鬧,春花似錦承匣、人聲如沸蓖乘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嘉抒。三九已至,卻和暖如春袍暴,著一層夾襖步出監(jiān)牢的瞬間些侍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工政模, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留娩梨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓览徒,卻偏偏與公主長得像狈定,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子习蓬,可洞房花燭夜當晚...
    茶點故事閱讀 45,455評論 2 359

推薦閱讀更多精彩內(nèi)容