Android框架 之 MVC, MVP, MVVM

Posted on By Vivian Sun

简介

介绍MVC, MVP, MVVM三者的区别。

同时用实例说明。

实例功能说明:从网站下载cofig数据,显示在页面上

1. MVC (Model View Controller)

MVC(Model View Controller)是软件框架中最常见的一种框架。

工作原理

当用户触发事件后,view层会发送指令到controller层。

接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上。

在Android上可以理解为:

  • Controller: Activity
  • View: layout.xml
  • Model: 存储数据和一些具体的逻辑操作。(实例中的DownloadApi, UrlConfig)

实例解析

activity_main.xml,两个控件,一个用来点击下载,一个用来显示数据

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="20dp"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_download"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Download" />

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

MainActivity.java

public class MainActivity extends BaseActivity {
    private static final String TAG = "MainActivity";

    private ProgressDialog mProcessDlg;
    private TextView mTVResult;

    // website
    private CompositeDisposable compositeDisposable = new CompositeDisposable();

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

    @Override
    protected void initData() {

    }

    @Override
    protected void initView(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);

        mTVResult = (TextView) findViewById(R.id.tv_result);

        findViewById(R.id.tv_download).setOnClickListener(v -> {
            showProgress();
            DownloadApi engine = (WebsiteApi) DownloadApi.INSTANCE;
            Disposable config = Observable.fromCallable(() -> engine.getConfig())
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(urlConfig -> {
                                final String result = engine.getGson().toJson(urlConfig.Debug).toString();
                                Log.d(TAG, "" + result);
                                mTVResult.setText(result);
                            },
                            error -> {
                                dismissProgress();
                            },
                            () -> {
                                Log.d(TAG, "tv_download complete");
                                dismissProgress();
                            });
            compositeDisposable.add(config);
        });

    }
  
    ......

    public void showProgress() {
        if (mProcessDlg == null) {
            mProcessDlg = ProgressDialog.show(this, "download", "I'm loading, please wait...");
        }
    }

    public void dismissProgress() {
        if (mProcessDlg != null) {
            mProcessDlg.dismiss();
        }
    }

}

DownloadApi.java

public enum DownloadApi {
    INSTANCE;
    private static final String TAG = "DownloadApi";

    private final OkHttpClient mOkHttpClient = new OkHttpClient();
    public final Gson mGson = new Gson();

    public static Gson getGson() {
        return INSTANCE.mGson;
    }

    /**
     * get config sync through okhttp
     */
    public UrlConfig getConfig() {
        try {
            Request request = new Request.Builder()
                    .url(Const.URL_CONFIG)
                    .build();

            Response response = mOkHttpClient.newCall(request).execute();
            if (!response.isSuccessful()) {
                Log.w(TAG, "getConfig failed " + response);
                return null;
            }
            Log.w(TAG, "getConfig " + response.body());
            UrlConfig config = mGson.fromJson(response.body().charStream(), UrlConfig.class);
            return config;
        } catch (Exception ex) {
            Log.w(TAG, "getConfig ex:", ex);
        }

        return null;
    }

}

我们从MVC角度分析下Code:

  • xml中有布局代码 (View)
  • activity作为一个Controller,里面的逻辑是监听用户点击并作出相应的操作,比如调用是调用WebsiteApi的方法去获取数据。
  • WebsiteApi,UrlConfig等类,则表示MVC中的model层,里面是数据和一些具体的逻辑操作

缺陷

从实例Code可以看出一些MVC的缺陷

  • activity的责任过重,既是controller又是view
    • 比如说有一个ProgressDialog的更新,其实是view层的逻辑,但是没办法写到xml里面
    • 比如TextView.setTextView()
    • 若是逻辑很复杂的页面,activity或者fragment是动辄上千行,维护起来相当累
  • 另外还有,view层和model层是相互可知的,两层之间存在耦合。对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。

2. MVP(Model View Presenter)

MVP作为MVC的演化,解决了MVC不少的缺点。

工作原理

view层发出的事件传递到presenter层中,presenter层去操作model层。

并且将数据返回给view层,整个过程中view层和model层完全没有联系。

在Android上可以理解为:

  • MVP的model层相对于MVC是一样的
  • activity和fragment不再是controller层,而是纯粹的view层。
    • activity,fragment可以实现定义好的接口,在对应的presenter中通过接口调用方法。
  • 所有关于用户事件的转发全部交由presenter层处理。

相较MVC最明显的差别就是view层和model层完全的解耦,取而代之的presenter层充当了桥梁的作用。

虽然view层和model层解耦了,但是view层和presenter层看起来耦合在一起了。其实不是的,对于view层和presenter层的通信,我们是可以通过接口实现。

最好的方式是:

  • fragment作为view层
  • activity则是用于创建view层(fragment)和presenter层(presenter)的一个控制器

还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。

实例解析

activity_main.xml,两个控件,一个用来点击下载,一个用来显示数据。跟MVC一样

MainActivity

 public class MainActivity extends BaseActivity implements IDownloadListener {
    private static final String TAG = "MainActivity";

    private ProgressDialog mProcessDlg;
    private TextView mTVResult;
    private DownloadPresenter mPresenter;

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

    @Override
    protected void initData() {
        mPresenter = new DownloadPresenter();
        mPresenter.setIDownloadListener(this);
    }

    @Override
    protected void initView(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);

        mTVResult = (TextView) findViewById(R.id.tv_result);
        findViewById(R.id.tv_download).setOnClickListener(v -> {
            showProgress();
            mPresenter.start();
        });
    }

    @Override
    protected void loadData() {

    }

    public void showProgress() {
        if (mProcessDlg == null) {
            mProcessDlg = ProgressDialog.show(this, "download", "I'm loading, please wait...");
        }
    }

    public void dismissProgress() {
        if (mProcessDlg != null) {
            mProcessDlg.dismiss();
        }
    }

    @Override
    public void onLoadStart() {
        showProgress();
    }

    @Override
    public void onLoadSuccess(String result) {
        dismissProgress();
        mTVResult.setText(result);
    }

    @Override
    public void onLoadFail(String error) {
        dismissProgress();
        mTVResult.setText("load error: " + error);
    }
}

DownLoadApi.java跟MVC中的一样

DownloadPresenter.java

public class DownloadPresenter {
    private static final String TAG = "DownloadPresenter";

    private IDownloadListener mLoadListener;

    // website
    private CompositeDisposable compositeDisposable = new CompositeDisposable();

    public void setIDownloadListener(IDownloadListener listener) {
        mLoadListener = listener;
    }

    public void get() {

    }

    public void start() {
        if (mLoadListener != null) {
            mLoadListener.onLoadStart();
        }
        DownLoadApi engine = (DownLoadApi) DownLoadApi.INSTANCE;
        Disposable config = Observable.fromCallable(() -> engine.getConfig())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(urlConfig -> {
                            final String result = engine.getGson().toJson(urlConfig).toString();
                            Log.d(TAG, "" + result);
                            if (mLoadListener != null) {
                                mLoadListener.onLoadSuccess(result);
                            }
                        },
                        error -> {
                            Log.w(TAG, "tv_download error:", error);
                            if (mLoadListener != null) {
                                mLoadListener.onLoadFail(error.toString());
                            }
                        },
                        () -> {
                            Log.d(TAG, "tv_download complete");
                        });
        compositeDisposable.add(config);
    }

    public void destroy() {
        // to avoid okhttp leak
        if (compositeDisposable != null && !compositeDisposable.isDisposed()) {
            compositeDisposable.dispose();
        }
    }

}

我们从MVP角度分析下Code:

  • 把activity的跟model层相关的逻辑抽取出来放在Presenter里面
  • Presenter里面在相应的时机调用接口,activity实现接口更新View

和MVC最大的不同是:MVP把activity作为了view层,整个activity没有任何和model层相关的逻辑代码。

取而代之的是把代码放到了presenter层中,presenter获取了model层的数据之后,通过接口的形式将view层需要的数据返回给它。

这样做到的好处:

  • activity的代码逻辑减少了
  • view层和model层完全解耦,也更方便测试
    • 比如你需要测试一个http请求是否顺利,你不需要写一个activity,只需要写一个java类,实现对应的接口,presenter获取了数据自然会调用相应的方法
    • 比如你也可以自己在presenter中mock数据,分发给view层,用来测试布局是否正确。

有个开源的库可以轻松实现MVP,有兴趣可以研究看看Mosby: Model-View-Presenter and Model-View-Intent library for Android apps.

3. MVVM (Model-View-ViewModel)

Model-View-ViewModel 就是将其中的 View 的状态和行为抽象化,让我们可以将UI和业务逻辑分开。

这些 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

google 推出了databinding ,使得MVVM的使用更方便了

工作原理

MVVM模式是通过以下三个核心组件组成:

  • Model - 包含了业务和验证逻辑的数据模型
  • View - 定义屏幕中View的结构,布局和外观
  • ViewModel - 扮演“View”和“Model”之间的使者,帮忙处理 View 的全部业务逻辑

view层和viewmodel层是相互绑定的,更新viewmodel层的数据,view层会相应的变动ui。

实例解析

gradle配置(2.3.3)

app build.gradle 中加入:

android {
......

    dataBinding {
        enabled true
    }

}

activity_main.xml

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

    <data>

        <variable
            name="result"
            type="com.vv.mvp.model.DownloadResult" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_download"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Download" />

        <TextView
            android:id="@+id/tv_result"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="50dp"
            android:text="@{result.result}" />

    </LinearLayout>
</layout>

DownloadResult.java (data binding class)

public class DownloadResult {
    public ObservableField<String> result = new ObservableField<>();
}

MainActivity.java (MVP的presenter没有移除)

public class MainActivity extends BaseActivity implements IDownloadListener {
    private static final String TAG = "MainActivity";

    private ProgressDialog mProcessDlg;
    private DownloadPresenter mPresenter;

    private ActivityMainBinding mBinding;
    private DownloadResult mResult;

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

    @Override
    protected void initData() {
        mPresenter = new DownloadPresenter();
        mPresenter.setIDownloadListener(this);
    }

    @Override
    protected void initView(Bundle savedInstanceState) {
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        findViewById(R.id.tv_download).setOnClickListener(v -> {
            showProgress();
            mPresenter.start();
        });
    }

    @Override
    protected void loadData() {

    }

    public void showProgress() {
        if (mProcessDlg == null) {
            mProcessDlg = ProgressDialog.show(this, "download", "I'm loading, please wait...");
        }
    }

    public void dismissProgress() {
        if (mProcessDlg != null) {
            mProcessDlg.dismiss();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        // to avoid dlg leak
        dismissProgress();

        mPresenter.destroy();
    }

    @Override
    public void onLoadStart() {
        showProgress();
    }

    @Override
    public void onLoadSuccess(String result) {
        dismissProgress();
        mResult.result.set(result);
    }

    @Override
    public void onLoadFail(String error) {
        dismissProgress();
        mResult.result.set("load error: " + error);
    }
}

我们从MVVM角度分析下Code:

  • 把view的更新交给binding来处理(ViewModel)

这里只是最简单的binding,实际上搭配recyclerview等可以省去非常多的view状态更新的code.

小结

  • 若项目不是非常复杂,使用android自己MVC问题不大。
  • 若是项目逻辑和模块较为复杂,为了避免Activity过于庞大,MVP是一个不错的选择。
  • MVP和MVVM一起使用对复杂和界面状态和行为逻辑复杂的项目,可以很大的降低耦合和维护成本。

Sample Code下载

Reference

以上图片均是从下面参看地址摘取,若有任何问题,请通知下,我会立即删除。

认清MVC,MVP和MVVM/

Android官方框架DataBinding

Android官方数据绑定框架DataBinding(一)

MVVM 模式介绍