美文网首页
Android架构模式之MVC、MVP、MVVM

Android架构模式之MVC、MVP、MVVM

作者: Sean1225 | 来源:发表于2019-03-16 10:42 被阅读0次

    在开始讲解各种架构模式时,我们先来看下没有经过设计的代码是如何编写的。为了不分散重点,笔者举的例子会比较简单,初始时从数据库缓存中获取用户信息展示到界面上,点击刷新按钮可以从服务器上拉取最新的用户信息并进行展示。

    由于从数据库和服务器上获取数据都属于更底层的逻辑,因此这两个操作一开始就会进行封装,不会列入讨论范围,并且为了使程序更加简单,这两个操作都是使用的测试代码进行模拟。

    User.java

    // User实体类,再没有封装意识的人,实体类总会有一个吧
    public class User {
        public String name;
        public int age;
    }
    

    DbUtils.java

    // 数据库工具
    public class DbUtils {
        // 查询数据库记录并返回cursor,这里使用测试代码直接返回null
        public static Cursor query(String sql) {
            return null;
        }
        
        // 更新数据库记录,这里使用测试代码不进行任何实际处理
        public static void update(String sql) {
            
        }
    }
    

    HttpUtils.java

    // http工具
    public class HttpUtils {
        private static Handler sHandler = new Handler(Looper.getMainLooper());
    
        public interface ResponseCallback {
            void onResponseSuccessed(String json);
            void onResponseFailed(int reason);
        }
    
        // 发起http请求,这里使用模拟的数据,并有一定机率请求失败
        public static void request(Map params, final ResponseCallback callback) {
            sHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    int value = new Random(System.currentTimeMillis()).nextInt(5);
                    if(callback != null) {
                        if(value == 2) {
                            callback.onResponseFailed(1);
                        } else {
                            callback.onResponseSuccessed("{\"name\": \"纯爷们\", \"age\": 20}");
                        }
                    }
                }
            }, 500);
        }
    }
    

    activity_user.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/tv_name"/>
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:id="@+id/tv_age"/>
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginTop="20dp"
            android:text="刷新"
            android:id="@+id/btn_refresh"/>
    
    </LinearLayout>
    

    UserActivity.java

    public class UserActivity extends Activity {
        private TextView mNameView;
        private TextView mAgeView;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_user);
    
            mNameView = (TextView)findViewById(R.id.tv_name);
            mAgeView = (TextView)findViewById(R.id.tv_age);
            
            // 加载缓存的用户数据并展示
            User user = loadUser();
            if(user != null) {
                mNameView.setText("昵称:" + user.name);
                mAgeView.setText("年龄:" + user.age);
            }
    
            findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 从服务器拉取最新用户数据并显示
                    refresh();
                }
            });
        }
    
        private User loadUser() {
            // 这里本应从cursor中获取数据,但为求程序尽量简单,我们直接使用模拟数据。之所以要加入query这段代码,是为了尽可能模拟真实的流程。
            Cursor cursor = DbUtils.query(null);
            if(cursor != null) {
                try {
    
                } catch (Exception e) {
    
                } finally {
                    cursor.close();
                }
            }
            User user = new User();
            user.name = "小虾米";
            user.age = 19;
            return user;
        }
    
        private void refresh() {
            HttpUtils.request(null, new HttpUtils.ResponseCallback() {
                @Override
                public void onResponseSuccessed(String json) {
                    try {
                        User user = new Gson().fromJson(json, User.class);
                        
                        // 用户信息更新了,要同步更新数据库中的记录
                        String updateSql = null;
                        DbUtils.update(updateSql);
                       
                        mNameView.setText("昵称:" + user.name);
                        mAgeView.setText("年龄:" + user.age);
                    } catch (Exception e) {
                    }
                }
    
                @Override
                public void onResponseFailed(int reason) {
                    Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    

    点击刷新前显示如下界面

    image
    点击刷新后显示如下界面
    image

    上面的例子请读者务必记劳,后续讲到的几种架构模式全部使用的都是这个例子。

    上述例子一个非常突出的问题是,用户信息可能在多个界面上都需要显示,而在这些界面上,从数据库和服务器上获取用户信息的流程都要写一遍,重复编写不仅容易出错也不容易维护。解决该问题的方法就是封装一个可复用的Model,MXX模式也由此产生。

    MVC

    MVC由Model、View、Controller组成,Android提供的xml布局是View层的主要部分,View本身已经是相对比较独立的了,因此MVC中主要考虑的就是Model的设计。我们来看下MVC中各层在Android中的主要应用:

    1. Model:表示模型层,模型的一个核心特点就是可复用。包含数据业务实体(entity/bean)本身,以及围绕该实体进行的业务操作(本地或远程增删改查操作)。即Model层主要针对数据,包含数据实体和数据访问,如果非要以模型来称呼和理解的话,前者为数据模型,后者为业务模型。
    2. View:表示视图层,负责界面数据的展示,以及响应用户操作。
    3. Controller:表示控制层,负责逻辑处理,由其连接Model和View。Controller通过Model获取数据并传递给View进行展示;通过响应View传递过来的用户事件调用Model的接口进行业务处理。

    后续内容都使用简称,M代表Model,V代表View,C代表Controller。

    其中,V和C一般又统称为UI层,由于Android已经提供了xml布局,因此在Android中V和C并不需要刻意区分,可以统一以UI层来理解,UI层的核心代码包含xml和Activity(或Fragment,或另外封装的控制器),后者既扮演着部分V的角色,又扮演着全部的C角色。我们来看下使用MVC重构后的例子,增加了UserBusiness类,修改了UserActivity的代码,以下只贴出更新的部分代码,其余代码请参考之前的例子。

    UserBusiness.java

    public class UserBusiness {
        private static final UserBusiness INSTANCE = new UserBusiness();
    
        private List<UserListener> mListeners = new LinkedList<>();
    
        public static UserBusiness get() {
            return INSTANCE;
        }
    
        public void addListener(UserListener listener) {
            if(listener == null) {
                return;
            }
            synchronized (mListeners) {
                if(!mListeners.contains(listener)) {
                    mListeners.add(listener);
                }
            }
        }
    
        public void removeListener(UserListener listener) {
            if(listener == null) {
                return;
            }
            synchronized (mListeners) {
                mListeners.remove(listener);
            }
        }
    
        public User getUser() {
            Cursor cursor = DbUtils.query(null);
            if(cursor != null) {
                try {
    
                } catch (Exception e) {
    
                } finally {
                    cursor.close();
                }
            }
            User user = new User();
            user.name = "小虾米";
            user.age = 19;
            return user;
        }
    
        public void requestUser() {
            HttpUtils.request(null, new HttpUtils.ResponseCallback() {
                @Override
                public void onResponseSuccessed(String json) {
                    User user = null;
                    try {
                        user = new Gson().fromJson(json, User.class);
                    } catch (Exception e) {
                    }
                    if(user != null) {
                        String updateSql = null;
                        DbUtils.update(updateSql);
                        notifyRequestUser(0, user);
                    } else {
                        notifyRequestUser(1, null);
                    }
                }
    
                @Override
                public void onResponseFailed(int reason) {
                    notifyRequestUser(reason, null);
                }
            });
        }
    
        private void notifyRequestUser(int code, User user) {
            List<UserListener> listeners = new LinkedList<>();
            synchronized (mListeners) {
                listeners.addAll(mListeners);
            }
            for(UserListener listener : listeners) {
                listener.onRequestUserResult(code, user);
            }
        }
    
        public interface UserListener {
            void onRequestUserResult(int code, User user);
        }
    }
    

    UserActivity.java

    public class UserActivity extends Activity implements UserBusiness.UserListener {
        private TextView mNameView;
        private TextView mAgeView;
        private UserBusiness mUserBusiness = UserBusiness.get();
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_user);
    
            mNameView = (TextView)findViewById(R.id.tv_name);
            mAgeView = (TextView)findViewById(R.id.tv_age);
    
            // 加载缓存的用户数据并展示
            User user = mUserBusiness.getUser();
            if(user != null) {
                mNameView.setText("昵称:" + user.name);
                mAgeView.setText("年龄:" + user.age);
            }
    
            findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 从服务器拉取最新用户数据并显示
                    mUserBusiness.requestUser();
                }
            });
    
            mUserBusiness.addListener(this);
        }
    
        @Override
        protected void onDestroy() {
            mUserBusiness.removeListener(this);
            super.onDestroy();
        }
    
        @Override
        public void onRequestUserResult(int code, User user) {
            if(code == 0) {
                mNameView.setText("昵称:" + user.name);
                mAgeView.setText("年龄:" + user.age);
            } else {
                Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
            }
        }
    }
    

    重构后的代码有如下优点:

    1. Activity的代码变得简单和整洁了,Activity现在只需要处理控制逻辑(UI逻辑),以及作为V和M通信的桥梁。
    2. 业务代码封装在UserBusiness中,一是隐藏了数据操作(业务流程)的具体细节,使得UI层在访问时更简单了;二是可以复用,任何模块都可以轻松访问,且可以通过在UserBusiness中注册一个监听器来监听用户业务的相关事件。

    但MVC仍具有以下缺点:

    1. V不可复用,然而在Android中V复用没有意义,需要复用的话完全可以封装可复用的控件,然后V组装这些控件。
    2. C不可复用。
    3. V和C之间还存在部分耦合,因此除了M外V和C都无法进行单元测试。

    为了解决以上缺点,便有了MVP。

    MVP

    MVP由Model、View和Presenter组成,M和V就不再重复解释了,P和C一样,承担着控制层的责任。MVP相比MVC作了如下改进(或者说变化,是否改进视情况而定):

    1. P可复用,这意味着不能再使用Activity(或...)作为P了,很简单,Activity不能复用(使用继承达到复用的场景不在这讨论范围之内)。由此,Activity从控制层的角色转向了视图层,即在MVP中V由xml和Activity组成。

      也可以另外抽离一个V,然后将Activity作为创建V和P的管理器,并负责将V和P进行绑定。但不建议采用这种方式,额外增加了代码,并且也没带来多少益处,除非想要复用V或者界面异常复杂而拆分了多个V和P。

    2. MVP三者皆可以独立完成单元测试,为了达到这个目的,P和V需要做到完全解耦,解耦一般使用接口。

    我们来看下使用MVP重构过的代码,在mvc的基础上主要是改动了UserActivity.java,然后增加了几个类。

    PresenterContext.java

    public interface PresenterContext {
        Activity getActivity();
    }
    

    UserPresenter.java

    public interface UserPresenter {
        void onRefresh();
        void onInited();
        void onDestroyed();
    }
    

    UserPresenterImpl.java

    public class UserPresenterImpl implements UserPresenter, UserBusiness.UserListener {
        private PresenterContext mContext;
        private UserView mView;
        private UserBusiness mUserBusiness = UserBusiness.get();
    
        public UserPresenterImpl(PresenterContext context, UserView view) {
            mContext = context;
            mView = view;
        }
    
        @Override
        public void onRefresh() {
            mUserBusiness.requestUser();
        }
    
        @Override
        public void onInited() {
            mUserBusiness.addListener(this);
            User user = mUserBusiness.getUser();
            if(user != null) {
                mView.updateName(user.name);
                mView.updateAge(user.age);
            }
    
        }
    
        @Override
        public void onDestroyed() {
            mUserBusiness.removeListener(this);
        }
    
        @Override
        public void onRequestUserResult(int code, User user) {
            if(code == 0) {
                mView.updateName(user.name);
                mView.updateAge(user.age);
            } else {
                Toast.makeText(mContext.getActivity(), "刷新失败", Toast.LENGTH_SHORT).show();
            }
        }
    }
    

    UserView.java

    public interface UserView {
        void updateName(String name);
        void updateAge(int age);
    }
    

    UserActivity.java

    public class UserActivity extends Activity implements UserView, PresenterContext {
        private TextView mNameView;
        private TextView mAgeView;
        private UserPresenter mPresenter;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_user);
    
            mNameView = (TextView)findViewById(R.id.tv_name);
            mAgeView = (TextView)findViewById(R.id.tv_age);
    
            mPresenter = new UserPresenterImpl(this, this);
    
            findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mPresenter.onRefresh();
                }
            });
    
            mPresenter.onInited();
        }
    
        @Override
        protected void onDestroy() {
            mPresenter.onDestroyed();
            super.onDestroy();
        }
    
        @Override
        public void updateName(String name) {
            mNameView.setText("昵称:" + name);
        }
    
        @Override
        public void updateAge(int age) {
            mAgeView.setText("年龄:" + age);
        }
    
        @Override
        public Activity getActivity() {
            return this;
        }
    }
    

    我们来讲解下:

    1. 由于Presenter不像Activity一样,它没有上下文信息,因此增加了PresenterContext类作为Presenter的上下文信息。

      这里PresenterContext只是用来获取Activity,是因为示例是力求简单,实际项目中它可以获取的信息会更多。

    2. UserActivity的代码被拆分成了两部分,一部分仍然在UserActivity中,作为View的代码,一部分抽离到UserPresenterImpl中,作为Presenter的代码。至此,将视图和控制层的代码完全分离了。
    3. 新建了UserViewUserPresenter两个接口用来表示V和P,V的实现方UserActivity持有UserPresenter接口而非具体的实现类,P的实现方持有UserView接口而非具体的实现类,从而达到V和P解耦。

      需要对P进行单元测试时,只需要创建一个类简单地实现UserView的类,然后和UserPresenterImpl绑定即可;需要对V进行单元测试时,只需要创建一个简单实现UserPresenter的类,然后在UserActivity中将构建P的那行代码修改下即可。

    MVP相对MVC具有P复用及方便做单元测试的优点,然而,在实际Android项目中,P复用的场景基本不存在,且多数公司并没有做单元测试。因此,多数情况下,MVC可能比MVP更适合Android项目,毕竟MVP多引入了不入类和代码,且带来解耦的同时也使得代码更加“绕”。

    MVVM

    不管是MVC还是MVP都存在几下问题:

    1. 在每个界面都要编写不少的findViewByIdsetOnClickListener之类的代码。
    2. 数据更新后,要手动调用setText之类的代码刷新视图。

    以上几点都不是什么大问题,编写这些代码也不会轻易出错,但总有达人追求极致,MVVM便由此产生了。Android解决第问题1的方法是在xml中直接嵌入代码(类似JSX的写法),解决问题2的方法是提供了DataBinding方案绑定视图和数据。MVVM真正地将V层完全地体现在xml上,M层还是老样子(模式怎么变它都不变),VM(ViewModel)用来代替C和P,以MVC作为改造,VM包括ActivityDataBinding(自动生成)。

    本文主要讲解几种架构模式的应用场景和区别,因此不会过多讲解MVVM在Android中如何使用,没接触过MVVM的建议先看下这篇入门文章或查阅官方文档。

    使用DataBinding需要在build.gradle中加入如下代码:

    android {
        dataBinding {
            enabled = true
        }
    }
    

    配置了之后build时会下载DataBinding的依赖包以及自动生成部分代码,自动生成的代码后续遇到时会提到。

    接着我们来看下在MVC的基础上变更后的MVVM代码。

    activity_user.xml

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    
        <data>
            <variable
                name="user"
                type="com.sean.mvvm.model.entity.User" />
    
            <variable
                name="host"
                type="com.sean.mvvm.UserActivity"/>
        </data>
    
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="10dp">
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text='@{"昵称:" + user.name}' />
    
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:text='@{"年龄:" + String.valueOf(user.age)}'/>
    
            <Button
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center"
                android:layout_marginTop="20dp"
                android:text="刷新"
                android:onClick="@{host.onRefresh}"/>
    
        </LinearLayout>
    
    </layout>
    

    User.java

    public class User extends BaseObservable {
        @Bindable
        public String name;
    
        @Bindable
        public int age;
    
        public void setName(String name) {
            this.name = name;
            notifyPropertyChanged(BR.name);
        }
    
        public void setAge(int age) {
            this.age = age;
            notifyPropertyChanged(BR.age);
        }
    }
    

    UserActivity.java

    public class UserActivity extends Activity implements UserBusiness.UserListener {
        private User mUser;
        private UserBusiness mUserBusiness = UserBusiness.get();
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
            mUser = mUserBusiness.getUser();
            if(mUser == null) {
                mUser = new User();
            }
            binding.setUser(mUser);
            binding.setHost(this);
    
            mUserBusiness.addListener(this);
        }
    
        @Override
        protected void onDestroy() {
            mUserBusiness.removeListener(this);
            super.onDestroy();
        }
    
        @Override
        public void onRequestUserResult(int code, User user) {
            if(code == 0) {
                mUser.setName(user.name);
                mUser.setAge(user.age);
            } else {
                Toast.makeText(UserActivity.this, "刷新失败", Toast.LENGTH_SHORT).show();
            }
        }
    
        public void onRefresh(View v) {
            mUserBusiness.requestUser();
        }
    }
    

    分析一下:

    1. xml布局的顶部标签变成了layoutdata标签存储非布局相关代码,variable定义变量。
    2. 编译时会根据xml名称和内容自动生成binding类,如例子中的ActivityUserBindingvariable定义的变量在binding中都有对应的set、get方法。
    3. 数据改变时要做到自动刷新UI,实体类必须继承BaseObservable,且需要自动触发的字段必须以Bindable注解,并在字段值变化时调用notifyPropertyChanged方法。

    PS:强烈建议至少熟读一个自动生成的binding类,绑定的所有原理都在这里,代码很容易理解,也不需要去网络上寻求答案。

    上面的例子实现了数据的单向绑定(数据更新触发UI更新)和事件的绑定,而Android是支持数据双向绑定的,现在来看下当UI更新时如何触发数据的更新。使用方式其实很简单,在xml中小小修改下就行:

    android:text='@={user.name}'
    

    注意到@后面多了个=,同时昵称:去掉了,=表示数据双向绑定,但当=存在时,右侧的表达式只能是个变量,因此昵称:只能去掉了。当xml改成这样后,TextView的文本发生变化了,user.name的值也会随之更新。

    总结下MVVM的优缺点:

    1. 新型xml更加成熟,可以独立支撑View层。然而,这可能也是缺点,毕竟这种xml编程方式和Android传统的方式差异较大。

      如果不想改变xml的编写方式,又希望使用MVVM,那么可以仿照自动生成的binding类自己编写一个ViewModel,但是有没有这个必要呢。。。

    2. 只关心数据变化,而不需要关注视图的刷新,刷新由自动生成的binding处理了。
    3. 数据双向绑定是个伪命题,实际上并没有完全做到自动化,还是需要手动编写额外的代码,并且也有条件限制。
    4. 在非主module(一般为app)中无法编译新型xml(这个也可能是笔者使用不当,有待确认)。

    综上,MVVM相比MVC、MVP并没有多大优势,但可以通知配置减少一些重复的逻辑代码。使用哪种模式根据实际情况而定,没有谁比谁更好。

    相关文章

      网友评论

          本文标题:Android架构模式之MVC、MVP、MVVM

          本文链接:https://www.haomeiwen.com/subject/bsmjmqtx.html