Android MVPVM架构实践

前言

写Android也有一段时间了,始终没有找到一种优雅流畅的Android架构模式,前不久看了google关于mvp架构的范例,甚好,建议仔细观摩一下:https://github.com/googlesamples/android-architecture。这篇文章也是参考了google的写法加上自己的理解和实践写出来的,供大家参考参考,如有不当的地方欢迎指正。

什么是MVPVM?

MVPVM=Model+View+Presenter+ViewModel
在没有使用类似MVP架构的时候,逻辑一般都直接写在了Activity或者Fragment里,导致View层很臃肿,业务逻辑、UI操作和数据耦合到了一起,结构混乱。现在,View层要处理的逻辑全部委托给Presenter处理,View层专注实现UI,Presenter专注实现业务逻辑,它们之间通过View Interface和Presenter Interface交互。在google推出databinding后,View Interface的部分功能可以转移到ViewModel中去,进一步降低View层的臃肿。

  • View层:实现View Interface,对外提供showDialog、showToast之类的方法
  • ViewModel层:以databinding为基础,对外提供控制xml界面的方法
  • Presenter层:实现Presenter Interface,处理业务逻辑
  • Model层:服务器数据对应数据模型类

实践

把下面这个页面(用户信息页面)以MVPVM模式写出来


用户信息页面

项目结构如下:


用户信息页面类结构

业务流程如下:


业务流程

View层触发了一个请求用户信息的event,然后View层将这个event交给Presenter层来处理,Presenter向服务器请求数据,Presenter拿到数据(数据被封装到了model里)后进行逻辑处理,然后根据需求操纵ViewModel进行UI更新。

1.Model层——UserInfoModel

以后台返回数据格式是json为例,这里的Model层就是一一对应的后台返回的数据。

public class UserInfoModel implements Serializable {
    /**
     * head : string
     * headBackground : string
     * name : string
     * sex : int 1:男 2:女
     * nationality : int 1:中国 2:美国
     * specialty : string
     * advantage : string
     * createTime : long
     */

    @SerializedName("head")
    private String head;
    @SerializedName("headBackground")
    private String headBackground;
    @SerializedName("name")
    private String name;
    @SerializedName("sex")
    private int sex;
    @SerializedName("nationality")
    private int nationality;
    @SerializedName("specialty")
    private String specialty;
    @SerializedName("advantage")
    private String advantage;
    @SerializedName("createTime")
    private long createTime;

    //下面是各个字段的get和set方法,不列出来了
    ...
}

2.ViewModel层——UserInfoViewModel

ViewModel相当于操作xml的代言人,任何xml显示的更新都要通过ViewModel来进行,要注意在写ViewModel的时候要完全按照xml来写,比如一个TextView要显示和隐藏,我会在ViewModel里定义一个int字段来表示;TextView要显示内容,我会在ViewModel里定义一个String字段来表示。ViewModel和xml之间用databinding绑定起来,操作ViewModel就相当于操作xml。如果对databinding不熟悉的请参考databinding google官方文档(不用科学上网也能看哦,Google给中国开发者的福利):
https://developer.android.google.cn/topic/libraries/data-binding/index.html

public class UserInfoViewModel extends BaseObservable {

    private int headBackgroundRes;
    private int headImageRes;
    private String name;
    private String sex;
    private String nationality;
    private String specialty;
    private String advantage;
    private String createTime;

    @Bindable
    public int getHeadImageRes() {
        return headImageRes;
    }

    public void setHeadImageRes(int headImageRes) {
        this.headImageRes = headImageRes;
        notifyPropertyChanged(BR.headImageRes);
    }

    @Bindable
    public int getHeadBackgroundRes() {
        return headBackgroundRes;
    }

    public void setHeadBackgroundRes(int headBackgroundRes) {
        this.headBackgroundRes = headBackgroundRes;
        notifyPropertyChanged(BR.headBackgroundRes);
    }

   ...
}

看一下xml中如何使用ViewModel的:

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

    <data>

        <variable
            name="viewModel"
            type="com.tc.mvpvmdemo.userinfo.UserInfoViewModel" />
    </data>

    <LinearLayout
        android:id="@+id/activity_user_info"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.tc.mvpvmdemo.userinfo.UserInfoActivity">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="10dp"
            bind:backgroundResource="@{viewModel.headBackgroundRes}"
            tools:background="@mipmap/bg_trump">

            <ImageView
                android:layout_width="80dp"
                android:layout_height="80dp"
                android:layout_gravity="center_horizontal"
                bind:imageResource="@{viewModel.headImageRes}"
                tools:src="@mipmap/ic_head" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginTop="20dp"
                android:text="@{viewModel.name}"
                android:textColor="@android:color/white"
                android:textSize="18sp"
                tools:text="川普" />
        </LinearLayout>

        <android.support.v4.widget.Space
            android:layout_width="match_parent"
            android:layout_height="20dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal"
            android:padding="8dp">

            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="性  别"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp"
                android:text="@{viewModel.sex}"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18sp"
                tools:text="男" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal"
            android:padding="8dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="国  籍"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp"
                android:text="@{viewModel.nationality}"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18sp"
                tools:text="美国" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal"
            android:padding="8dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="特  长"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp"
                android:text="@{viewModel.specialty}"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18sp"
                tools:text="表情包" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal"
            android:padding="8dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="优  势"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp"
                android:text="@{viewModel.advantage}"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18sp"
                tools:text="女儿" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:orientation="horizontal"
            android:padding="8dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="创建时间"
                android:textSize="18sp" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp"
                android:text="@{viewModel.createTime}"
                android:textColor="@color/colorPrimaryDark"
                android:textSize="18sp"
                tools:text="2016.12.16 12:34" />
        </LinearLayout>
    </LinearLayout>
</layout>

可以发现大部分字段ViewModel和Model是能对上的,有的人会图简单把Model和ViewModel合二为一,不过不建议这么做,不要在Model或者ViewModel中写任何逻辑,因为ViewModel可以把前后端分离,也就是说,只要后台接口定好了,Presenter就可以使用Model、View和ViewModel将整个业务逻辑写完。即使最后后台字段变化,影响的也只是Presenter中的处理逻辑,如果将Model和ViewModel合在一起,当后台字段变化,除了修改Presenter、Model外,xml和ViewModel也得修改,得不偿失。

3.Presenter层——UserInfoPresenter

Presenter层是处理业务逻辑的核心,它处理由View层转移过来的事件,逻辑处理完毕后操作View和ViewModel更新UI。IUserInfo描述了Presenter和View层的接口定义,Presenter和View层就是通过这些接口进行交互的。

public interface IUserInfo {
    interface IView {
        void updateTitle(String title);//更新页面的标题

        void showDialog(String content);//显示一个dialog

        void closeDialog();//关闭dialog
    }

    interface IPresenter {
        void onViewInit();//页面初始化后执行
    }
}

Presenter和View层只关心对方提供了那些接口,而不关心对方的具体实现细节,我可以通过接口的不同实现来实现不同的UI和不同的业务逻辑。

public class UserInfoPresenter implements IUserInfo.IPresenter {
    private IUserInfo.IView mView;
    private UserInfoViewModel mViewModel;

    public UserInfoPresenter(IUserInfo.IView iView, UserInfoViewModel viewModel) {
        this.mView = iView;
        this.mViewModel = viewModel;
    }

    @Override
    public void onViewInit() {
        mView.updateTitle("用户信息");
        requestData();
    }

    private UserInfoModel mockTrump() {
        UserInfoModel trump = new UserInfoModel();
        trump.setHead("xxx.jpg");
        trump.setHeadBackground("xxxx.jpg");
        trump.setName("川普");
        trump.setNationality(2);
        trump.setSex(1);
        trump.setSpecialty("表情包");
        trump.setAdvantage("漂亮的女儿");
        trump.setCreateTime(System.currentTimeMillis());
        return trump;
    }

    private void requestData() {
        mView.showDialog("正在获取数据");
        //不建议这么用Handler,这里只是模拟网络请求的延迟
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                mView.closeDialog();
                updateUi(mockTrump());
            }
        }, 2000);
    }

    private void updateUi(UserInfoModel model) {
        mViewModel.setName(model.getName());
        mViewModel.setHeadImageRes(R.mipmap.ic_head);
        mViewModel.setHeadBackgroundRes(R.mipmap.bg_trump);
        String sex;
        switch (model.getSex()) {
            case 1:
                sex = "男";
                break;
            case 2:
                sex = "女";
                break;
            default:
                sex = "不详";
        }
        mViewModel.setSex(sex);
        String nationality;
        switch (model.getNationality()) {
            case 1:
                nationality = "中国";
                break;
            case 2:
                nationality = "美国";
                break;
            default:
                nationality = "地球";
        }
        mViewModel.setNationality(nationality);
        mViewModel.setAdvantage(model.getAdvantage());
        mViewModel.setSpecialty(model.getSpecialty());
        mViewModel.setAdvantage(model.getAdvantage());
        mViewModel.setCreateTime(new SimpleDateFormat("yyyy.MM.dd HH:mm").format(new Date(model.getCreateTime())));
    }
}

在页面初始化完成后控制权就已经由View转移到Presenter,Presenter根据逻辑执行了一次获取用户数据的网络请求,收到服务器返回的数据后通过ViewModel更新UI。

4.View层——UserInfoActivity

UserInfoActivity实现了IView接口,所以本例中View层就是UserInfoActivity,它初始化了Presenter和databinding,并持有Presenter的引用,当需要处理业务逻辑时会调用Presenter来处理。如在本例中,UserInfoActivity实现如何更新title,和如何显示和关闭一个dialog。

public class UserInfoActivity extends AppCompatActivity implements IUserInfo.IView {

    ActivityUserInfoBinding binding;
    UserInfoPresenter presenter;
    ProgressDialog dialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_user_info);
        UserInfoViewModel viewModel = new UserInfoViewModel();
        presenter = new UserInfoPresenter(this, viewModel);
        binding.setViewModel(viewModel);
        presenter.onViewInit();
    }

    @Override
    public void updateTitle(String title) {
        this.setTitle(title);
    }

    @Override
    public void showDialog(String content) {
        if (dialog == null) {
            dialog = new ProgressDialog(this);
        }
        dialog.setMessage(content);
        dialog.show();
    }

    @Override
    public void closeDialog() {
        if (dialog != null && dialog.isShowing()) {
            dialog.dismiss();
        }
    }
}

最后附上demo地址:https://github.com/wunianhub/Android-MVPVM

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

推荐阅读更多精彩内容