Android開發(fā)中MVP模式的應(yīng)用

一舱殿、什么是 MVP

1.1. MVP 的定義

MVP,全稱 Model-View-Presenter 模型-視圖-表示器
隨著UI創(chuàng)建技術(shù)的功能日益增強,UI層也履行著越來越多的職責(zé)。為了更好地細分視圖(View)與模型(Model)的功能,讓View專注于處理數(shù)據(jù)的可視化以及與用戶的交互,同時讓Model只關(guān)系數(shù)據(jù)的處理,基于MVC(Model-View-Controller)概念的MVP模式應(yīng)運而生。

1.2. 為什么需要 MVP

理由1:盡量簡單

大部分的安卓應(yīng)用只使用View-Model結(jié)構(gòu)
程序員現(xiàn)在更多的是和復(fù)雜的View打交道而不是解決業(yè)務(wù)邏輯抵蚊。
當你在應(yīng)用中只使用Model-View時,到最后,你會發(fā)現(xiàn)“所有的事物都被連接到一起”。

image.png

只使用 Model-View
如果這張圖看上去還不是很復(fù)雜斯够,那么請你想象一下以下情況:每一個View在任意一個時刻都有可能出現(xiàn)或者消失掖桦。不要忘記View的保存和恢復(fù)怔昨,在臨時的view上掛載一個后臺任務(wù)矮烹。
“所有的事物都被連接到一起”的替代品是一個萬能對象(god object)桑驱。

image.png

god object
god object是十分復(fù)雜的赊级,他的每一個部分都不能重復(fù)利用押框,無法輕易的測試、或者調(diào)試和重構(gòu)此衅。
那么MVP呢强戴?

image.png

使用 MVP
復(fù)雜的任務(wù)被分成細小的任務(wù)亭螟,并且很容易解決。越小的東西骑歹,bug越少预烙,越容易debug,更好測試道媚。在MVP模式下的View層將會變得簡單扁掸,所以即便是他請求數(shù)據(jù)的時候也不需要回調(diào)函數(shù)。View邏輯變成十分直接最域。

理由2:后臺任務(wù)

當你編寫一個Actviity谴分、Fragment、自定義View的時候镀脂,你會把所有的和后臺任務(wù)相關(guān)的方法寫在一個靜態(tài)類或者外部類中牺蹄。這樣,你的Task不再和Activity聯(lián)系在一起薄翅,這既不會導(dǎo)致內(nèi)存泄露沙兰,也不依賴于Activity的重建。
這里有若干種方法處理后臺任務(wù)翘魄,但是它們的可靠性都不及MVP鼎天。

1.3. MVP 的優(yōu)缺點

任何事務(wù)都存在兩面性,MVP當然也不列外暑竟,我們來看看MVP的優(yōu)缺點斋射。
優(yōu)點:

  1. 降低耦合度,實現(xiàn)了Model和View真正的完全分離但荤,可以修改View而不影響Modle
  2. 模塊職責(zé)劃分明顯罗岖,層次清晰(下面會介紹Bob大叔的Clean Architecture)
  3. 隱藏數(shù)據(jù)
  4. Presenter可以復(fù)用,一個Presenter可以用于多個View腹躁,而不需要更改Presenter的邏輯(當然是在View的改動不影響業(yè)務(wù)邏輯的前提下)
  5. 利于測試驅(qū)動開發(fā)呀闻。以前的Android開發(fā)是難以進行單元測試的(雖然很多Android開發(fā)者都沒有寫過測試用例,但是隨著項目變得越來越復(fù)雜潜慎,沒有測試是很難保證軟件質(zhì)量的捡多;而且近幾年來Android上的測試框架已經(jīng)有了長足的發(fā)展——開始寫測試用例吧),在使用MVP的項目中Presenter對View是通過接口進行铐炫,在對Presenter進行不依賴UI環(huán)境的單元測試的時候垒手。可以通過Mock一個View對象倒信,這個對象只需要實現(xiàn)了View的接口即可科贬。然后依賴注入到Presenter中,單元測試的時候就可以完整的測試Presenter應(yīng)用邏輯的正確性。
  6. View可以進行組件化榜掌。在MVP當中优妙,View不依賴Model。這樣就可以讓View從特定的業(yè)務(wù)場景中脫離出來憎账,可以說View可以做到對業(yè)務(wù)完全無知套硼。它只需要提供一系列接口提供給上層操作。這樣就可以做到高度可復(fù)用的View組件胞皱。
  7. 代碼靈活性
    缺點:
  8. Presenter中除了應(yīng)用邏輯以外邪意,還有大量的View->Model,Model->View的手動同步邏輯反砌,造成Presenter比較笨重雾鬼,維護起來會比較困難。
  9. 由于對視圖的渲染放在了Presenter中宴树,所以視圖和Presenter的交互會過于頻繁策菜。
  10. 如果Presenter過多地渲染了視圖,往往會使得它與特定的視圖的聯(lián)系過于緊密酒贬。一旦視圖需要變更做入,那么Presenter也需要變更了。
  11. 額外的代碼復(fù)雜度及學(xué)習(xí)成本同衣。

1.4. 小結(jié)

在MVP模式里通常包含4個要素:
(1) View:負責(zé)繪制UI元素、與用戶進行交互(在Android中體現(xiàn)為Activity);
(2) View interface:需要View實現(xiàn)的接口壶运,View通過View interface與Presenter進行交互耐齐,降低耦合,方便進行單元測試;
(3) Model :負責(zé)存儲蒋情、檢索埠况、操縱數(shù)據(jù)(有時也實現(xiàn)一個Model interface用來降低耦合);
(4) Presenter :作為View與Model交互的中間紐帶,處理與用戶交互的負責(zé)邏輯棵癣。

二辕翰、MVX 剖析

2.1. M(Model)

模型:表示數(shù)據(jù)模型和業(yè)務(wù)邏輯(business logic)。
model層主要負責(zé):

  • 從網(wǎng)絡(luò)狈谊,數(shù)據(jù)庫喜命,文件,傳感器河劝,第三方等數(shù)據(jù)源讀寫數(shù)據(jù)壁榕。
  • 對外部的數(shù)據(jù)類型進行解析轉(zhuǎn)換為APP內(nèi)部數(shù)據(jù)交由上層處理。
  • 對數(shù)據(jù)的臨時存儲,管理赎瞎,協(xié)調(diào)上層數(shù)據(jù)請求牌里。

2.2 V(View)

視圖:將數(shù)據(jù)呈現(xiàn)給用戶。
view 層主要負責(zé):

  • 提供UI交互
  • 在presenter的控制下修改UI务甥。
  • 將業(yè)務(wù)事件交由presenter處理牡辽。
    注意: View層不存儲數(shù)據(jù)喳篇,不與Model層交互。
    在Android中View層一般是Activity态辛、Fragment麸澜、View(控件)、ViewGroup(布局等)等因妙。

三痰憎、MVP與MVC的異同

MVC模式與MVP模式都作為用來分離UI層與業(yè)務(wù)層的一種開發(fā)模式被應(yīng)用了很多年。在我們選擇一種開發(fā)模式時攀涵,首先需要了解一下這種模式的利弊:
無論MVC或是MVP模式都不可避免地存在一個弊端:
額外的代碼復(fù)雜度及學(xué)習(xí)成本铣耘。
這就導(dǎo)致了這兩種開發(fā)模式也許并不是很小型應(yīng)用。
但比起他們的優(yōu)點以故,這點弊端基本可以忽略了:
(1)降低耦合度
(2)模塊職責(zé)劃分明顯
(3)利于測試驅(qū)動開發(fā)
(4)代碼復(fù)用
(5)隱藏數(shù)據(jù)
(6)代碼靈活性

對于MVP與MVC這兩種模式蜗细,它們之間也有很大的差異。有一些程序員選擇不使用任何一種模式怒详,有一部分原因也許就是不能區(qū)分這兩種模式差異炉媒。以下是這兩種模式之間最關(guān)鍵的差異:
MVP模式:

  • View不直接與Model交互,而是通過與Presenter交互來與Model間接交互
  • Presenter與View的交互是通過接口來進行的昆烁,更有利于添加單元測試
  • 通常View與Presenter是一對一的吊骤,但復(fù)雜的View可能綁定多個Presenter來處理邏輯
    MVC模式:
  • View可以與Model直接交互
  • Controller是基于行為的,并且可以被多個View共享
  • 可以負責(zé)決定顯示哪個View

四静尼、利用MVP進行Android開發(fā)的例子

說了這么多理論白粉,現(xiàn)在輪到實踐了。
現(xiàn)在我們來實現(xiàn)這樣一個Android上的Demo(如圖):可以從EditText讀取用戶信息并存取鼠渺,也可以根據(jù)ID來從后臺讀出用戶信息并顯示鸭巴。

7A898D38A14787DB48F3B6C67C99D344.jpg

頁面布局很簡單,就不介紹了拦盹。下面根據(jù)MVP原則來進行編碼:
先來看看java文件的目錄結(jié)構(gòu):

image.png

可以發(fā)現(xiàn)鹃祖,Presenter與Model、View都是通過接口來進行交互的普舆,既降低耦合也方便進行單元測試恬口。

(1)首先我們需要一個UserBean,用來保存用戶信息

package com.bbno.mvp2.bean;

public class UserBean {
    private String firstName;
    private String lastName;

    public UserBean(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "UserBean{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                '}';
    }
}

(2)再來看看View接口:
根據(jù)需求可知沼侣,View可以對ID楷兽、FirstName、LastName這三個EditText進行讀操作华临,對FirstName和LastName進行寫操作芯杀,由此定義IUserView接口:

package com.bbno.mvp2.view;

public interface IUserView {
    int getID();
    String getFirstName();
    String getLastName();
    void setFirstName(String firstName);
    void setLastName(String lastName);
}

(3)Model接口:
同樣,Model也需要對這三個字段進行讀寫操作,并存儲在某個載體內(nèi)(這不是我們所關(guān)心的揭厚,可以存在內(nèi)存却特、文件、數(shù)據(jù)庫或者遠程服務(wù)器筛圆,但對于Presenter及View無影響),定義IUserModel接口:

package com.bbno.mvp2.model;

import com.bbno.mvp2.bean.UserBean;

public interface IUserModel {
    void setID(int id);
    void setFirstName(String firstName);
    void setLastName(String lastName);
    UserBean load(int id);//通過id讀取user信息,返回一個UserBean
}

定義類UserModel實現(xiàn)接口IUserModel:

package com.bbno.mvp2.model;

import android.util.Log;
import android.util.SparseArray;
import com.bbno.mvp2.bean.UserBean;

public class UserModel implements IUserModel{

    private SparseArray<UserBean> mUserArray = new SparseArray<>();
    private int mID;
    private String mFirstName;
    private String mLastName;

    @Override
    public void setID(int id) {
        mID = id;
    }

    @Override
    public void setFirstName(String firstName) {
        mFirstName = firstName;
    }

    @Override
    public void setLastName(String lastName) {
        mLastName = lastName;
        UserBean mUserBean = new UserBean(mFirstName, mLastName);
        mUserArray.append(mID, mUserBean);
    }

    @Override
    public UserBean load(int id) {
        mID = id;
        UserBean userBean = mUserArray.get(mID, new UserBean("no found", "no found"));
        return userBean;
    }
}

(4)Presenter:
至此裂明,Presenter就能通過接口與View及Model進行交互了:

package com.bbno.mvp2.presenter;

import com.bbno.mvp2.bean.UserBean;
import com.bbno.mvp2.model.IUserModel;
import com.bbno.mvp2.model.UserModel;
import com.bbno.mvp2.view.IUserView;

public class UserPresenter {

    private IUserModel mUserModel;
    private IUserView mUserView;

    public UserPresenter(IUserView mUserView) {
        this.mUserView = mUserView;
        this.mUserModel = new UserModel();
    }

    public void saveUser(int id, String firstName, String lastName){
        mUserModel.setID(id);
        mUserModel.setFirstName(firstName);
        mUserModel.setLastName(lastName);
    }

    public void loadUser(int id){
        UserBean user = mUserModel.load(id);
        mUserView.setFirstName(user.getFirstName());
        mUserView.setLastName(user.getLastName());
    }
}

(5)UserActivity:
UserActivity實現(xiàn)了IUserView接口,同時有一個UserPresenter成員變量:

package com.bbno.mvp2.view;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;

import com.bbno.basic.R;
import com.bbno.mvp2.presenter.UserPresenter;

import butterknife.Bind;
import butterknife.ButterKnife;
import cn.wwah.basekit.base.activity.BaseActivity;

public class UserActivity extends BaseActivity implements IUserView{

    private UserPresenter mUserPresenter = new UserPresenter(this);
    @Bind(R.id.edt_id)
    EditText edt_id;
    @Bind(R.id.edt_first)
    EditText edt_first;
    @Bind(R.id.edt_last)
    EditText edt_last;
    @Bind(R.id.btn_save)
    Button btn_save;
    @Bind(R.id.btn_read)
    Button btn_read;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);
        ButterKnife.bind(this);
        initUI();
    }

    private void initUI() {
        btn_save.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserPresenter.saveUser(getID(), getFirstName(), getLastName());
            }
        });
        btn_read.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserPresenter.loadUser(getID());
            }
        });
    }

    @Override
    public int getID() {
        return Integer.parseInt(edt_id.getText().toString());
    }

    @Override
    public String getFirstName() {
        return edt_first.getText().toString();
    }

    @Override
    public String getLastName() {
        return edt_last.getText().toString();
    }

    @Override
    public void setFirstName(String firstName) {
        edt_first.setText(firstName);
    }

    @Override
    public void setLastName(String lastName) {
        edt_last.setText(lastName);
    }
}

可以看到太援,View只負責(zé)處理與用戶進行交互闽晦,并把數(shù)據(jù)相關(guān)的邏輯操作都扔給了Presenter去做。而Presenter調(diào)用Model處理完數(shù)據(jù)之后提岔,再通過IUserView更新View顯示的信息仙蛉。

View剩下的方法及UserModel類不是我們所關(guān)心重點。

(6)activity_user.xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#fff"
    tools:context="com.bbno.mvp2.view.UserActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="ID:"
            android:textSize="16sp"/>

        <EditText
            android:id="@+id/edt_id"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10"
            android:textSize="16sp"
            android:inputType="textPersonName"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:text="FirstName"
            android:textSize="16sp"
            android:gravity="center"/>

        <EditText
            android:id="@+id/edt_first"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10"
            android:textSize="16sp"
            android:inputType="textPersonName" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/textView2"
            android:layout_width="80dp"
            android:layout_height="wrap_content"
            android:text="LastName"
            android:textSize="16sp"
            android:gravity="center"/>

        <EditText
            android:id="@+id/edt_last"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:ems="10"
            android:textSize="16sp"
            android:inputType="textPersonName"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_save"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="16sp"
            android:text="保存" />

        <Button
            android:id="@+id/btn_read"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textSize="16sp"
            android:text="讀取" />
    </LinearLayout>
</LinearLayout>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碱蒙,一起剝皮案震驚了整個濱河市荠瘪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赛惩,老刑警劉巖哀墓,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喷兼,居然都是意外死亡篮绰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門季惯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吠各,“玉大人,你說我怎么就攤上這事星瘾。” “怎么了惧辈?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵琳状,是天一觀的道長。 經(jīng)常有香客問我盒齿,道長念逞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任边翁,我火速辦了婚禮翎承,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘符匾。我一直安慰自己叨咖,他們只是感情好,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著甸各,像睡著了一般垛贤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趣倾,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天聘惦,我揣著相機與錄音,去河邊找鬼儒恋。 笑死善绎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的诫尽。 我是一名探鬼主播禀酱,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼箱锐!你這毒婦竟也來了比勉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤驹止,失蹤者是張志新(化名)和其女友劉穎浩聋,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體臊恋,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡衣洁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了抖仅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坊夫。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖撤卢,靈堂內(nèi)的尸體忽然破棺而出环凿,到底是詐尸還是另有隱情,我是刑警寧澤放吩,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布智听,位于F島的核電站,受9級特大地震影響渡紫,放射性物質(zhì)發(fā)生泄漏到推。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一惕澎、第九天 我趴在偏房一處隱蔽的房頂上張望莉测。 院中可真熱鬧,春花似錦唧喉、人聲如沸捣卤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腌零。三九已至梯找,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間益涧,已是汗流浹背锈锤。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留闲询,地道東北人久免。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像扭弧,于是被迫代替她去往敵國和親阎姥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

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