Android MVC、MVP、MVVM、MVP-databinding 架构单元示例

GitHub地址:ProjectPatternStudy

基本Android项目都采用MVC、MVP、MVVM架构,个人认为软件架构没有绝对的优劣之分,大家都各有利弊。

  • 如果页面比较单一,采用MVC也未尝不可;
  • 如果需要稳定性高,解耦性强就可以选用MVP,使M层与V层分离,结构更清晰;
  • 如果想尝鲜(其实已经有段时间了),少写接口,高效,也可以使用MVVM;

阮一峰《MVC,MVP 和 MVVM 的图示》总结的非常简练,这里相当于扩展了一下,对于不太懂的人可能会用处更大。

MVP-databinding:是使用MVP架构,但是布局使用databinding设置值,也是行之有效的一种,也可以满足你的需求。

MVC

Model-View-Controller,最常见的软件架构之一。

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑
  • 模型(Model):数据保存
来自MVC----MVC,MVP 和 MVVM 的图示

Avtivity里的一个点击事件:

/**
 * 将业务逻辑封装在Model里, 但C(Activity)层可以和Model直接交互, 交互完后根据显示结果来调整V层(如 显示数据)
 */
EssayModel essayModel = new EssayModel(LoadDataActivity.this);
essayModel.getEssay(3, new EssayModel.OnEssayListener() {
    @Override
    public void onSuccess(List<Essay> list) {
        /**
         * 直接使用list,得到List的逻辑都放在mode层
         */
        if (list != null && list.get(0) != null) {
            tvViewUpdata.setText("MVC 更新数据: " + list.get(0).getTitle());
        }
    }

    @Override
    public void onError() {

    }
});

如果一个页面比较简单,只有简单的几个操作,也不会经常去改可以使用此方式;如果页面逻辑比较复杂,接口请求都有好几个,那么不建议使用MVC,因为代码会全部堆积在一个Activity里面,会显得非常之冗余。

MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。


来自MVP----MVC,MVP 和 MVVM 的图示

通过P层将Model层与View层解耦,同时P与V、P与M可以相互通信。
下面举个登录的例子:

public class UserLoginActivity extends AppCompatActivity implements IUserLoginView {


    @BindView(R.id.et_username)
    EditText etUsername;
    @BindView(R.id.et_password)
    EditText etPassword;
    @BindView(R.id.bt_login)
    Button btLogin;
    @BindView(R.id.bt_clear)
    Button btClear;
    @BindView(R.id.progress)
    ProgressBar progress;
    @BindView(R.id.activity_user_login)
    RelativeLayout activityUserLogin;

    private UserLoginPresenter userLoginPresenter = new UserLoginPresenter(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_login);
        ButterKnife.bind(this);
        setTitle("用户登录(MVP)");

        initListener();

    }

    private void initListener() {
        btLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                userLoginPresenter.login();
            }
        });
        btClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                userLoginPresenter.clear();
            }
        });
    }

    @Override
    public String getUserName() {
        return etUsername.getText().toString().trim();
    }

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

    @Override
    public void clearUserName() {
        etUsername.setText("");
    }

    @Override
    public void clearPassword() {
        etPassword.setText("");
    }

    @Override
    public void showLoading() {
        progress.setVisibility(View.VISIBLE);
    }

    @Override
    public void hindLoading() {
        progress.setVisibility(View.GONE);
    }

    @Override
    public void toMainActivity() {
        Toast.makeText(this, "login success , to MainActivity!", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFailedError() {
        Toast.makeText(this, "login failed!", Toast.LENGTH_SHORT).show();
    }
}
/**
 * Created by jingbin on 2016/11/3.
 * Presenter是用作Model和View之间交互的桥梁,那么应该有什么方法呢?
 * 其实也是主要看该功能有什么操作,比如本例,两个操作:login和clear。
 */

public class UserLoginPresenter {

    // view
    private IUserLoginView iUserLoginView;
    // model
    private UserBiz mUserBiz;
    private Handler mHandler = new Handler();

    public UserLoginPresenter(IUserLoginView iUserLoginView) {
        this.iUserLoginView = iUserLoginView;
        this.mUserBiz = new UserBiz();
    }

    public void login() {
        //view
        iUserLoginView.showLoading();
        // model
        mUserBiz.login(iUserLoginView.getUserName(), iUserLoginView.getPassword(), new OnLoginListener() {
            @Override
            public void loginSuccess(User user) {

                // 需要在UI线程中执行
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        iUserLoginView.toMainActivity();
                        iUserLoginView.hindLoading();
                    }
                });
            }

            @Override
            public void loginFailed() {
                // 需要在UI线程中执行
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        iUserLoginView.hindLoading();
                        iUserLoginView.showFailedError();
                    }
                });
            }
        });
    }

    public void clear() {
        iUserLoginView.clearUserName();
        iUserLoginView.clearPassword();
    }

}
public interface IUserLoginView {

    // login说明了要有用户名、密码,那么对应两个方法:
    String getUserName();

    String getPassword();

    void clearUserName();

    void clearPassword();

    // 再者login是个耗时操作,我们需要给用户一个友好的提示,一般就是操作ProgressBar,所以再两个:
    void showLoading();

    void hindLoading();


    // login当然存在登录成功与失败的处理,我们主要看成功我们是跳转Activity,而失败可能是去给个提醒:
    void toMainActivity();

    void showFailedError();
}

用户点击登录,触发点击事件,然后通过P层userLoginPresenter,调用登录的方法login(),方法里面会通过Model层mUserBiz.login()去做一些数据请求操作的处理,然后得到相应的数据返回。这里看到Model层的数据处理操作放在P层里,是不与V层直接交互的。
然后M层得到数据后回调,P层根据相应的数据,显示不同的UI,如toMainActivity,showFailedError等,这样V层只会出现一些基本的显示逻辑的处理。

MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。


来自MVVM----MVC,MVP 和 MVVM 的图示

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

/**
 * 简单的MVVM + data-binding案例:
 * 以点击一下按钮然后年龄会+2 为例
 *
 * @author jingbin
 */
public class ChangeAgeActivity extends AppCompatActivity {

    private ChangeAgeViewModel viewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityChangeAgeBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_change_age);
        setTitle("MVVM + data-binding");

        viewModel = ViewModelProviders.of(this).get(ChangeAgeViewModel.class);
        binding.setViewModel(viewModel);
        binding.setButtonname("年龄+2");

        /**监听年龄的变化*/
        viewModel.desc.observe(this, new Observer<String>() {
            @Override
            public void onChanged(@Nullable String desc) {
                Log.e("desc", desc);
            }
        });

        binding.btAge.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                viewModel.change();
            }
        });

    }
}
/**
 * @author jingbin
 */
public class ChangeAgeViewModel extends AndroidViewModel {

    final MutableLiveData<String> desc = new MutableLiveData<>();
    public final ObservableField<String> age = new ObservableField<>();

    public ChangeAgeViewModel(@NonNull Application application) {
        super(application);
        age.set(String.valueOf(23));
    }

    void change() {
        String value = age.get();
        if (!TextUtils.isEmpty(value)) {
            Integer integer = Integer.valueOf(value);
            // 改变age的值 布局里的值直接改变
            age.set(String.valueOf(integer + 2));

            desc.setValue("年龄改变:" + age.get());
        }
    }

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

    <!-- View + ViewModel-->
    <data>

        <variable
            name="viewModel"
            type="com.example.jingbin.projectstru.mvvm.ChangeAgeViewModel" />

        <variable
            name="buttonname"
            type="String" />

    </data>


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


        <TextView
            android:id="@+id/tv_mvvm"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="年龄"
            android:textColor="@color/colorPrimary" />

        <TextView
            android:id="@+id/tv_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp"
            android:text="@{viewModel.age}" />

        <Button
            android:id="@+id/bt_age"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{buttonname}" />

    </LinearLayout>
</layout>

可以看出,MVVM比MVP少了对应View的接口文件,这样更简洁了,而且,改变ViewModel里的值,则xml文件对应的值也会对应改变。如果通过手动setText(),则ViewModel里的值也会得到改变。通过这一层关系,我们可以通过数据去操控View里的显示,所以才可以去除掉对应View的接口文件。

MVP-databinding

基本实现了MVC,MVP,MVVM后,我发现它们各自有各自的优缺点。
MVC:简单,单一页面可以实现。但是不利于复杂页面。
MVP:解耦,结构清晰。但文件较多,每一个页面基本要新建P层和V层的文件,同时还会有findViewById操作。
MVVM:解耦,结构相对清晰,文件相对MVP较少。但如果页面显示比较复杂,需要通过多个值去控制页面的显示,或者页面一个值的显示 要通过多种逻辑去处理得到结果,个人感觉还是不太适用。(其中的ViewModel与对应宿主的生命周期相同,从而内存泄漏问题比MVP处理较好这里先不做讨论)

MVP-databinding
处理方式与MVP相同,只是使用了databinding的优势,databinding节省了类似findViewById和数据绑定的时间,从此代码里就没有findViewById和ButterKnife之类的代码了,而且也不会有通过多个值去控制页面的显示这样不好操作的情况了。当然文件还是会多一些。

/**
 * MVP + data-binding
 *
 * @author jingbin
 */
public class MvpDataBindingActivity extends AppCompatActivity implements ChangeAgeView {

    private ActivityMvpDataBindingBinding binding;
    private ChangeAgePresenter presenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_mvp_data_binding);

        setTitle("MVP + data-binding");
        presenter = new ChangeAgePresenter(this);
        binding.setButtonname("年龄+2");

        binding.btAge.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                presenter.changeAge(binding.getUser());
            }
        });
    }

    @Override
    public void showContentView(UserBean user) {
        binding.setUser(user);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        presenter.clear();
    }
}
/**
 * @author jingbin
 * @date 2019/02/26
 */

public class ChangeAgePresenter {

    private ChangeAgeView changeInterface;
    private UserModel userModel;

    public ChangeAgePresenter(ChangeAgeView changeInterface) {
        this.changeInterface = changeInterface;
        // 初始化
        changeInterface.showContentView(new UserBean("小白", 23));
    }

    /**
     * 改变年龄
     */
    public void changeAge(UserBean myUser) {
        if (userModel == null) {
            userModel = new UserModel();
        }
        userModel.changeAge(myUser, 2, new UserModel.ChangeInterface() {
            @Override
            public void success(UserBean user) {
                changeInterface.showContentView(user);
            }
        });
    }

    public void clear() {
        userModel = null;
    }
}

参考资料

End

对应项目:ProjectPatternStudy 😁
此文仅个人总结,如有不当之处,请留言告知。

projectstru
├─ MainActivity.java
├─ mvc
│    ├─ LoadDataActivity.java
│    ├─ bean
│    │    └─ Essay.java
│    └─ model
│           ├─ EssayModel.java
│           └─ MainModel.java
├─ mvp
│    ├─ UserLoginActivity.java
│    ├─ bean
│    │    └─ User.java
│    ├─ model
│    │    ├─ IUserBiz.java
│    │    ├─ OnLoginListener.java
│    │    └─ UserBiz.java
│    ├─ presenter
│    │    └─ UserLoginPresenter.java
│    └─ view
│           └─ IUserLoginView.java
├─ mvpdatabindind
│    ├─ MvpDataBindingActivity.java
│    ├─ bean
│    │    └─ UserBean.java
│    ├─ model
│    │    └─ UserModel.java
│    ├─ presenter
│    │    └─ ChangeAgePresenter.java
│    └─ view
│           └─ ChangeAgeView.java
└─ mvvm
       ├─ ChangeAgeActivity.java
       └─ ChangeAgeViewModel.java
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343