Android開發架構選擇MVP or MVVM

寫在前面的幾句話

<p>
最近有打算寫一些獨立的App的打算,所以對現在的Android架構與技術的選擇進行了重新的思考,拋開那些亂七八糟的東西,以一個極客的態度去選擇。

首先我們來看看Android開發中的幾大架構

一.MVC,MVP,MVVM的區分

<p>
1.MVC

傳統的Android App其實都是基于MVC的,Activity,Fragment相當于C,布局相當于V,數據邏輯相當于M

隨著業務的增長Controller里的代碼會越來越臃腫,因為它不只要負責業務邏輯,還要控制View的展示。也就是說Activity、Fragment雜糅了Controller和View,耦合變大。并不能算作真正意義上的MVC。

這也是為什么后面的MVP會引起很多開發者興趣的原因了。

2.MVP

MVP架構其實可以說與MVC的架構還是有很大的差別的,數據邏輯相當于M,Activity(負責View的繪制以及與用戶交互)相當于V ,View于Model間的交互則為P

理論上感覺區別有點抽象,可以通過下面的圖來看一看其中的區別

圖1 MVC與MVP的區別

其實最明顯的區別就是,MVC中是允許Model和View進行交互的,而MVP中很明顯,Model與View之間的交互由Presenter完成。還有一點就是Presenter與View之間的交互是通過接口的

3.MVVM

MVVM是Model-View-ViewModel的簡寫. 它是有三個部分組成:Model、View、ViewModel。Model:數據模型層。包含業務邏輯和校驗邏輯,View:屏幕上顯示的UI界面(layout、views),ViewModel:View和Model之間的鏈接橋梁,處理視圖邏輯。

當View有用戶輸入后,ViewModel通知Model更新數據,同理Model數據更新后,ViewModel通知View更新。

MVVM其實與MVP架構看起來很相似

圖2 MVP與MVVM

這里可以可以看到 ViewModel 承擔了 Presenter 中與 view和 Model 交互的職責,與 MVP模式不同的是,VM與 V 之間是通過 Datebinding 實現的,而 P是持有 View 的對象,直接調用 View 中的一些接口方法來實現。ViewModel可以理解成是View的數據模型和Presenter的合體。它通過雙向綁定(松耦合)解決了MVP中Presenter與View聯系比較緊密的問題,然而Android中的Datebinding只能單向綁定,只能從ViewModel綁定到View中

二 .MVP and MVVM代碼對比

<p>
接下來就對同一功能用MVP與MVVM架構來實現,通過代碼對比,分析到底選擇哪一種比較好

代碼很簡單就用登錄的簡單邏輯

圖1 demo截圖

Mvp代碼實現

首先看下結構

  • 數據邏輯相當于M
  • Activity(負責View的繪制以及與用戶交互)相當于V
  • View與Model間的交互則為P
圖2 Mvp代碼結構

從M開始

UserModel.Class

public class UserModel {

    private String username;
    private String password;

    public UserModel(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int checkUserValidity(String username, String password) {
        if (username == null || password == null ||
                username.isEmpty() ||
                password.isEmpty()) {
            return -1;
        }
        return 0;
    }
}

接下來是V

ILoginView.Class

public interface ILoginView {

    void showProgress();

    void hideProgress();

    void setPasswordError();

    String getUsername();

    String getPassword();

    void loginSuccess();

}

LoginActivity.Class

public class LoginActivity extends AppCompatActivity implements ILoginView,View.OnClickListener{

    private EditText usernameEdit,passwrodEdit;

    private Button loginButton;

    ProgressDialog pd;

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

        pd = new ProgressDialog(this);

        usernameEdit = (EditText) findViewById(R.id.et_username);
        passwrodEdit = (EditText) findViewById(R.id.et_username);
        loginButton = (Button) findViewById(R.id.bt_login);

        loginButton.setOnClickListener(this);

    }

    @Override
    public void showProgress() {
        pd.show();
    }

    @Override
    public void hideProgress() {
        pd.cancel();
    }

    @Override
    public void setPasswordError() {
        passwrodEdit.setError("passwrod error");
    }

    @Override
    public String getUsername() {
        return usernameEdit.getText().toString();
    }

    @Override
    public String getPassword() {
        return passwrodEdit.getText().toString();
    }

    @Override
    public void loginSuccess() {
        Toast.makeText(this, "login success", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.bt_login:
               
                break;
        }
    }

最后就是P了

ILoginPresenter.Class

public interface ILoginPresenter {

    void Login(String username, String password);

}

LoginPresenter.Class

public class LoginPersenter implements ILoginPresenter{

    private ILoginView loginView;
    private UserModel mUser;


    public LoginPersenter(ILoginView loginView) {
        this.loginView = loginView;
        initUser();
    }

    private void initUser(){
        mUser = new UserModel(loginView.getUsername(),loginView.getPassword());
    }

    @Override
    public void Login(String username, String password) {
        loginView.showProgress();
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                loginView.hideProgress();
                int code = mUser.checkUserValidity(loginView.getUsername(), loginView.getPassword());
                if (code == -1) {
                    loginView.setPasswordError();
                } else if (code == 0) {
                    loginView.loginSuccess();
                }
            }
        },2000);
    }

}

最后在LoginActivity中補上P的調用

//初始化
loginPresenter = new LoginPersenter(this);

//Click方法中的調用
loginPresenter.Login(usernameEdit.getText().toString(),passwrodEdit.getText().toString());

到這里MVP模式的代碼就已經實現了

Mvvm代碼實現

  • Model:數據模型層。包含業務邏輯和校驗邏輯
  • View:屏幕上顯示的UI界面(layout、views)
  • ViewModel:View和Model之間的鏈接橋梁,處理視圖邏輯。
圖3 Mvvm代碼結構

從M開始

UserModel.Class

public class UserModel extends BaseObservable{

    private String username;
    private String password;

    public UserModel(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Bindable
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
        notifyPropertyChanged(BR.username);
    }

    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
        notifyPropertyChanged(BR.password);
    }

    public int checkUserValidity() {
        if (username == null || password == null ||
                username.isEmpty() ||
                password.isEmpty()) {
            return -1;
        }
        return 0;
    }
}

接下來是V

activity_mvvmlogin.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="user"
            type="com.netease.mvpormvvmdemo.mvvm.UserModel"/>

    </data>


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

        <EditText
            android:id="@+id/et_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />


        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Button
            android:id="@+id/bt_login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Login"
            />

    </LinearLayout>

</layout>

最后是VM了

LoginActivity.Class

public class LoginActivity extends AppCompatActivity{

    ActivityMvvmloginBinding binding;
    ProgressDialog pd;
    UserModel userModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_mvvmlogin);
        pd = new ProgressDialog(this);

        binding.btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                userModel = new UserModel(binding.etUsername.getText().toString(),binding.etPassword.getText().toString());
                binding.setUser(userModel);
                doLoign();
            }
        });
    }

    private void doLoign(){
        pd.show();
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                pd.cancel();
                int code = userModel.checkUserValidity();
                if (code == -1) {
                    binding.etPassword.setError("passwrod error");
                } else if (code == 0) {
                    Toast.makeText(getBaseContext(), "login success", Toast.LENGTH_SHORT).show();
                }
            }
        },2000);
    }
}

到這里MVVM的代碼也實現了

是不是看到這里覺得這不是很坑爹嘛?MVVM的寫法和以前MVC的寫法基本一樣呀?這樣還有什么意義?

確實一樣,這是為什么呢?其實之前介紹的時候也有提到過Android中的Datebingding只能單向綁定,只能從ViewModel綁定到View中,所以呢View中數據的變化我們在ViewModel中并不能拿到,所以寫法和MVC沒有什么區別

但是我們可以從ViewModel綁定到View中,這里其實就有很大的變化了

那我們修改一下登陸邏輯,登錄后清除界面元素信息,顯示成功頁或者失敗頁

MVP架構的代碼就不書寫了,和之前的是一樣結構的,主要是來看下MVVM從ViewModel綁定到View中

首先修改下Model

public class UserModel extends BaseObservable{

    private String username;
    private String password;
    private int status = 1;

    @Bindable
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
        notifyPropertyChanged(BR.username);
    }

    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
        notifyPropertyChanged(BR.password);
    }

    @Bindable
    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
        notifyPropertyChanged(BR.status);
    }

    public void checkUserValidity() {
        if (username == null || password == null ||
                username.isEmpty() ||
                password.isEmpty()) {
            setStatus(-1);
        }else {
            setStatus(0);
        }
    }
}

這里主要是增加了一個新的屬性為status,這個屬性主要是用作判斷當前界面的狀態

接下來看看view的修改

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <import type="android.view.View"/>

        <variable
            name="user"
            type="com.netease.mvpormvvmdemo.mvvm.UserModel"/>

    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >

        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="@{user.status == 1 ? View.VISIBLE : View.GONE}"
            >

            <EditText
                android:id="@+id/et_username"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />


            <EditText
                android:id="@+id/et_password"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

            <Button
                android:id="@+id/bt_login"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Login"
                />

        </LinearLayout>


        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:visibility="@{user.status == 0 ? View.VISIBLE : View.GONE}"
            >

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{user.username}"
                />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{user.password}"
                />

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:visibility="@{user.status == -1 ? View.VISIBLE : View.GONE}"
            >

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="error"
                />

        </LinearLayout>

    </RelativeLayout>

</layout>

view內主要是修改為三個布局,根據status的狀態進行變化

最后看看viewmodel的變化,viewmodel其實沒有怎么變化,因為model里面小的修改了下

private void doLoign(){
    pd.show();
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            pd.cancel();
            userModel.checkUserValidity();
        }
    },2000);
}

這里的代碼只有這些

當我們輸入后點擊login后,model會對輸入狀態取檢測,從而改變model里面的status屬性,而通過ViewModel,view會及時拿到最新的狀態,從而通過判斷去做view層的變化。這就是MVVM的綁定機制了

看下運行截圖

圖4 Mvvm

三.對比結論與分析

<p>
通過代碼的對比,明顯可以發現Mvp模式下的代碼量相對來說確實增加了很多,但是邏輯相對的更加清晰,所以我覺得Mvp模式不是很適合小型的項目,小型項目整一堆類出來確實不是很好的事情,但是如果是一個較大型的項目還是可以選用這種架構來做開發,畢竟邏輯清晰,維護起來也比較方便。

而Mvvm的架構我個人覺得是Android往后發展的趨勢,畢竟谷歌都推出了Datebinding,而使用Datebinding也就可以不用去使用bufferknife了,而且綁定的這種機制也確實帶來了model,view與vm的分離,從邏輯上看也確實清晰了很多,問題是暫時只支持單向綁定,這個也要等谷歌后面的更新了,而且代碼的閱讀性會下降很多,所以呢這個比較時候小型的極客項目,暫時不適合大型的項目。

寫在后面的幾句話

其實架構性的東西在我看來沒有一定的固定的寫法,包括mvp和mvvm也可以根據自己的業務和理解去做不同的代碼區分,像前陣子也有人提出了基于MVP與MVVM的mvvmp的架構,所有架構是死的,人是活的,記得面試聽過一個大神說要跳出設計模式的圈圈,會有很多新的發現。。。共勉

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容