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阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容