Mvvm、RxJava、Retrofit 三剑合璧

废话

说起现在Android流行的app架构,脱口而出MVP、MVVM,要问两者区别,张口就来,balabalabala。。但是公司所有项目用的都是MVP,从没正式用过MVVM,所以对MVVM并没有深刻的理解。今天闲下来,赶紧整理下MVVM的头绪。当然光说MVVM是不行的,肯定要讲一讲网络请求框架Retrofit,Retrofit和RxJava通常都是配套使用,所以这篇文章就把三者串在一起讲。

简介

MVVM的全称就是Model、View、ViewModel,View就是视图,对应Activity和xml文件,纯粹的ui展示层,不涉及任何的业务流程;Model就是数据模型,ViewModel就是最重要的一层结构,从名字就可以看出,它是连接View和Model的桥梁,它会把数据更新到ui上,也会接受来自ui的交互事件,并处理相应的业务。

这里涉及到了一个更新ui的操作,和以前不同的是,它不会获取到控件然后更细ui,而是依赖DataBinding来实现双向绑定。Databinding 是一种框架,MVVM是一种模式,两者的概念是不一样的。DataBinding是一个实现数据和UI绑定的框架,只是一个实现MVVM模式的工具。ViewModel和View可以通过DataBinding来实现单向绑定和双向绑定,这套UI和数据之间的动态监听和动态更新的框架Google已经帮我们做好了。在MVVM模式中ViewModel和View是用绑定关系来实现的,所以有了DataBinding 使我们构建Android MVVM 应用程序成为可能。

  • 数据驱动
    在MVVM中,以前开发模式中必须先处理业务数据,然后根据的数据变化,去获取UI的引用然后更新UI,通过也是通过UI来获取用户输入,而在MVVM中,数据和业务逻辑处于一个独立的ViewModel中,ViewModel只要关注数据和业务逻辑,不需要和UI或者控件打交道。由数据自动去驱动UI去自动更新UI,UI的改变又同时自动反馈到数据,数据成为主导因素,这样使得在业务逻辑处理只要关心数据,方便而且简单很多。

  • 低耦合度
    MVVM模式中,数据是独立于UI的,ViewModel只负责处理和提供数据,UI想怎么处理数据都由UI自己决定,ViewModel 不涉及任何和UI相关的事也不持有UI控件的引用,即使控件改变(TextView 换成 EditText)ViewModel 几乎不需要更改任何代码,专注自己的数据处理就可以了,如果是MVP遇到UI更改,就可能需要改变获取UI的方式,改变更新UI的接口,改变从UI上获取输入的代码,可能还需要更改访问UI对象的属性代码等等。

  • 更新 UI
    在MVVM中,我们可以在工作线程中直接修改ViewModel的数据(只要数据是线程安全的),剩下的数据绑定框架帮你搞定,很多事情都不需要你去关心。

  • 团队协作
    MVVM的分工是非常明显的,由于View和ViewModel之间是松散耦合的。一个是处理业务和数据,一个是专门的UI处理。完全有两个人分工来做,一个做UI(xml 和 Activity)一个写ViewModel,效率更高。

  • 可复用性
    一个ViewModel复用到多个View中,同样的一份数据,用不同的UI去做展示,对于版本迭代频繁的UI改动,只要更换View层就行,对于如果想在UI上的做AbTest 更是方便的多。

  • 单元测试
    ViewModel里面是数据和业务逻辑,View中关注的是UI,这样的做测试是很方便的,完全没有彼此的依赖,不管是UI的单元测试还是业务逻辑的单元测试,都是低耦合的。

实战Demo

我在学一个新东西时,习惯从写demo开始,单纯地看代码太枯燥,也容易忘,但是自己动手写一个demo,就比较容易理解的深刻。

我们一起写一个很简单的小demo,类似一个社交软件,有四个页面:用户信息、好友列表、聊天记录、粉丝列表

Demo页面
  • 用户信息:这个页面很简单,主要是演示常规页面数据绑定的写法
  • 好友列表:这个页面主要演示RecyclerView列表页面数据绑定的写法
  • 聊天:这个页面主要演示多类型RecyclerView列表Adapter的写法
  • 粉丝列表:从网络获取数据,主要演示RxJava+Retrofit的基本使用方法

构建

理论讲了这么多,不废话了,下面就开始搭建MVVM的应用程序。

gradle

dataBinding的数据bangdingGoogle已经帮我们做好了,我们只要在android节点下加入配置即可

android {
    ...
    dataBinding {
        enabled = true
    }
    ...
}

UserInfoActivity

像往常一样创建一个Activity,但是setContentView的时候,我们要调用DataBindingUtil的静态方法:

public class UserInfoActivity extends BaseActivity {

    private ActivityUserInfoBinding mUserInfoBinding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUserInfoBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_info);
    }
}

DataBindingUtil.setContentView方法返回一个ActivityUserInfoBinding对象,这个类是哪来的呢?
不要慌,这个类是自动编译生成的,根据layout的名字,把下横线去掉,然后首字母大写,最后在加上一个Binding,比如我们传的layout名字叫:activity_user_info,根据规则生成的类名就叫ActivityUserInfoBinding。

activity_user_info.xml

下面看下layout的代码,看完再解释什么意思

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

    <data>
        <variable
            name="userInfoViewModel"
            type="com.makeunion.mvvmrxjavaretrofit.userinfo.UserInfoViewModel"/>
    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:padding="16dp"
        android:background="#ffffff">

        <ImageView
            android:id="@+id/head_img"
            android:layout_width="84dp"
            android:layout_height="84dp"
            android:layout_marginTop="16dp"
            img:imgurl="@{userInfoViewModel.headImageUrl}"
            img:placeholder="@{@color/color_place_holder}"
            img:error="@{@color/color_place_holder}"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginTop="32dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:textStyle="bold"
                    android:text="姓名:"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:text="@{userInfoViewModel.name}"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginTop="16dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:textStyle="bold"
                    android:text="年龄:"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:text="@{String.valueOf(userInfoViewModel.age)}"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginTop="16dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:textStyle="bold"
                    android:text="职业:"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:text="@{userInfoViewModel.job}"/>
            </LinearLayout>

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:layout_marginTop="16dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textSize="16sp"
                    android:textStyle="bold"
                    android:text="爱好:"/>

                <android.support.v7.widget.RecyclerView
                    android:id="@+id/hobby_recycler_view"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:overScrollMode="never"
                    app:data="@{userInfoViewModel.hobbies}"/>
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</layout>

和我们通常的xml布局不一样,最外层是一个layout节点,然后包含两个子节点,第一个是data,第二个是LinearLayout。
LinearLayout就是我们正常的布局,data就是数据绑定的ViewModel,type是ViewModel的全路径,name是变量名,后面布局中就是用这个name访问ViewModel的数据。

UserInfoViewModel

既然说到ViewModel,那就来看下代码

public class UserInfoViewModel extends BaseViewModel<UserInfoActivity> {

    public ObservableField<String> headImageUrl = new ObservableField<>();

    public ObservableField<String> name = new ObservableField<>();

    public ObservableInt age = new ObservableInt();

    public ObservableField<String> job = new ObservableField<>();

    public ObservableField<List<HobbyViewModel>> hobbies = new ObservableField<>();

    public UserInfoViewModel(UserInfoActivity activity) {
        super(activity);
    }

    public void loadUserInfo() {
        headImageUrl.set("https://avatar.csdn.net/6/F/7/1_u011002668.jpg?1523959103");
        name.set("朱小明");
        age.set(21);
        job.set("三山街贴膜");

        List<HobbyViewModel> list = new ArrayList<>();
        HobbyViewModel model1 = new HobbyViewModel("爬树");
        HobbyViewModel model2 = new HobbyViewModel("吃被门夹过的核桃");
        HobbyViewModel model3 = new HobbyViewModel("把头伸进微波炉");
        list.add(model1);
        list.add(model2);
        list.add(model3);
        hobbies.set(list);
    }
}

ViewModel是MVVM里最复杂的一层,一点点来看,首先看成员变量。这些成员变量都是ObservableField类型或者ObservableInt类型,泛型包着的才是layout需要的数据, 为什么要用Observable包一下呢,就是为了更新ui,看下面的loadUserInfo方法,当我们调用name.set("朱小明");时,会自动通知ui更新。在这个demo中,一开始页面是没有数据的,当我们在Activity中调用ViewModel的loadUserInfo后,dataBinding会把我们设置的假数据更新到ui上。

UserInfoActivity

下面再回到Activity,看Activity是怎么调用ViewModel的。

public class UserInfoActivity extends BaseActivity {

    private ActivityUserInfoBinding mUserInfoBinding;

    private UserInfoViewModel mUserInfoViewModel;

    private CommonAdapter<HobbyViewModel> mHobbiesAdapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mUserInfoBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_info);
        mUserInfoViewModel = new UserInfoViewModel(this);
        mUserInfoBinding.setUserInfoViewModel(mUserInfoViewModel);

        mUserInfoBinding.hobbyRecyclerView.setLayoutManager(
                new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        mHobbiesAdapter = new CommonAdapter<>(R.layout.list_item_hobby, BR.hobbyViewModel);
        mUserInfoBinding.hobbyRecyclerView.setAdapter(mHobbiesAdapter);

        mUserInfoViewModel.loadUserInfo();
    }
}

在Activity中创建了一个UserInfoViewModel对象,然后调用mUserInfoBinding.setUserInfoViewModel方法设置给了mUserInfoBinding,那么问题又来了,这个setUserInfoViewModel是哪来的?答案也是自动编译生成的,生成规则就是根据layout.xml中ViewModel的name,前面加上set构成方法名。

我们回头看下前面的layout,ViewModel的name是userInfoViewModel,生成的方法名就是setUserInfoViewModel()。调用了set方法之后,View视图层就和ViewModel层绑定了。在onCreate方法的最后调用mUserInfoViewModel.loadUserInfo();加载数据,数据加载完,ui即会自动更新。

通过以上几个流程,我们就实现了最基本的页面:


用户信息

RecyclerView

仔细看上面的代码,涉及到了RecyclerView,但是并没有做解释,下面详细讲下RecyclerView列表怎么实现数据绑定。

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

    <data>

        <variable
            name="friendListViewModel"
            type="com.makeunion.mvvmrxjavaretrofit.friendlist.FriendListViewModel" />
    </data>

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

        <android.support.v7.widget.RecyclerView
            android:id="@+id/friend_list_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:overScrollMode="never"
            app:data="@{friendListViewModel.mFriendListViewModel}"/>
    </LinearLayout>
</layout>

看完第一个例子,再看这个布局就很简单了,结构都一样,只不过这里的主布局是RecyclerView,先看下Activity里是怎么设置RecyclerView的。

public class FriendListActivity extends BaseActivity {

    private ActivityFriendListBinding mFriendListBinding;

    private FriendListViewModel mFriendListViewModel;

    private CommonAdapter<FriendViewModel> mFriendsAdapter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mFriendListBinding = DataBindingUtil.setContentView(this, R.layout.activity_friend_list);
        
        mFriendListViewModel = new FriendListViewModel(this);
        mFriendListBinding.setFriendListViewModel(mFriendListViewModel);

        mFriendListBinding.friendListRecyclerView.setLayoutManager(
                new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        mFriendListBinding.friendListRecyclerView.addItemDecoration(new VerticalDecoration(this));

        mFriendsAdapter = new CommonAdapter<>(R.layout.list_item_friend, BR.friendViewModel);
        mFriendListBinding.friendListRecyclerView.setAdapter(mFriendsAdapter);

        mFriendListViewModel.loadFriendList();
    }
}

我们惊讶的发现mFriendListBinding居然可以直接访问RecyclerView对象,其实和前面的setViewModel方法一样,这也是根据xml中控件名自动生成的。接下来给RecyclerView设置LayoutManager和Decoration,这都没什么说的。

紧接着就是设置Adapter,这里值得说一下。我这里用的是CommonAdapter,它是怎么实现的呢?

CommonAdapter

用MVVM的方式写Adapter和以前的写法是不一样的,看下完整代码

public class CommonAdapter<T> extends RecyclerView.Adapter<CommonAdapter.CustomViewHolder> {

    List<T> mData = new ArrayList<>();

    private int mLayoutId = -1;
    private int mVariableId = -1;

    public CommonAdapter() {
    }

    /**
     * 通用Adapter的构造函数
     *
     * @param layoutId   item的布局文件ID
     * @param variableId 布局文件中data中的变量ID,eg. 变量名为viewModel,则这里传值为BR.viewModel
     */
    public CommonAdapter(int layoutId, int variableId) {
        mLayoutId = layoutId;
        mVariableId = variableId;
    }

    public void setData(List data) {
        if (data != null) {
            this.mData = data;
            notifyDataSetChanged();
        }
    }

    public void addData(List<T> data) {
        if (data != null) {
            int ps = mData.size();
            mData.addAll(data);
            notifyItemRangeInserted(ps, data.size());
        }
    }

    /**
     * 设置布局文件ID
     *
     * @param layoutId Item布局文件ID
     */
    public void setLayoutId(int layoutId) {
        mLayoutId = layoutId;
    }

    /**
     * 设置布局文件中Data部分变量ID,eg.  变量名为viewModel,则这里传值为BR.viewModel
     *
     * @param variableId Data部分变量ID
     */
    public void setVariableId(int variableId) {
        mVariableId = variableId;
    }

    @Override
    public CommonAdapter.CustomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (mLayoutId != -1 && mVariableId != -1) {
            ViewDataBinding binding = DataBindingUtil.inflate(
                    LayoutInflater.from(parent.getContext()), mLayoutId, parent, false);
            return new CustomViewHolder(binding);
        } else {
            throw new IllegalArgumentException("No layoutId & variableId !!!");
        }
    }

    @Override
    public void onBindViewHolder(CommonAdapter.CustomViewHolder holder, int position) {
        T itemInfo = mData.get(position);
        holder.mBinding.setVariable(mVariableId, itemInfo);
        holder.mBinding.executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    public static class CustomViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding mBinding;

        public CustomViewHolder(@NonNull ViewDataBinding binding) {
            super(binding.getRoot());
            this.mBinding = binding;
        }
    }
}

如果仅仅是写一个RecyclerView的Adapter,其实并不需要这么复杂,我是把它加了泛型写成了通用Adapter,仔细看代码,和以前的Adapter在大体结构上是一致的,不同的主要是两点:

  • 加载布局 onCreateViewHolder()
  • 绑定数据 onBindViewHolder()

加载布局时,我们要用DataBindingUtil.inflate()方法,该方法返回一个ViewDataBinding对象,然后把传递给ViewHolder,传统的写法ViewHolder持有的是一个View,而这里是一个ViewDataBinding。

绑定数据时,不同于以前直接给控件赋值的方式,而是调用了ViewDataBinding的setVariable(mVariableId, itemInfo)和executePendingBindings()方法,这种方式和前面的例子是一样的,都是把View和ViewModel绑定在一起,只不过这种写法比较手工。

mVariableId是什么呢?它其实是xml中申明的ViewModel的id。比如我们在xml中申明了一个ViewModel,name叫friendListViewModel,就会自动在BR类中编译出一个id,叫BR.friendViewModel,Activity中在new CommonAdapter时,就是传递的这个值。

FriendListViewModel

既然说到了FriendListViewModel,那就来看看这个类是怎么写的。

public class FriendListViewModel extends BaseViewModel<FriendListActivity> {

    public ObservableField<List<FriendViewModel>> mFriendListViewModel = new ObservableField<>();

    public FriendListViewModel(FriendListActivity activity) {
        super(activity);
    }

    public void loadFriendList() {
        List<FriendViewModel> friends = new ArrayList<>();
        friends.addAll(requestData());
        mFriendListViewModel.set(friends);
    }

    private List<FriendViewModel> requestData() {
        List<FriendViewModel> data = new ArrayList<>();
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_01, "张三", "打南边来了个喇嘛"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_02, "李四", "手里提着五斤鳎蚂"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_03, "王五", "打北边来了一个哑巴"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_04, "赵四", "腰里别着一个喇叭"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_05, "刘能", "提搂鳎蚂的喇嘛要拿鳎蚂去换别着喇叭的哑巴的喇叭"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_06, "大脚", "别着喇叭的哑巴不愿意拿喇叭去换提搂鳎蚂的喇嘛的鳎蚂"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_07, "芙蓉", "提搂鳎蚂的喇嘛抡起鳎蚂就给了别着喇叭的哑巴一鳎蚂"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_08, "秀才", "别着喇叭的哑巴抽出喇叭就给了提搂鳎蚂的喇嘛一喇叭"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_09, "掌柜", "也不知是提搂鳎蚂的喇嘛打了别着喇叭的哑巴"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_10, "大嘴", "还是别着喇叭的哑巴打了提搂鳎蚂的喇嘛"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_11, "展堂", "喇嘛回家炖鳎蚂"));
        data.add(new FriendViewModel(mActivity).setData(R.drawable.head_image_12, "小六", "哑巴回家滴滴答答吹喇叭"));
        return data;
    }
}

这个ViewModel只有一个成员变量,仍然是ObservableField类型,泛型是List<FriendViewModel>,这个FriendViewModel又是什么呢?它也是一个ViewModel,但是它是RecyclerView每一个item的ViewModel,item在加载时也和普通布局一样,也是通过绑定一个ViewModel来加载数据的,看到这就明白了吧,这就是MVVM的风格,xml只管展示,xml绑定一个ViewModel,数据都来自ViewModel,ViewModel处理业务逻辑并通过DataBinding更新数据。

好友列表

多类型RecyclerView

我们要写一个通用的多类型Adapter,其实和上面单类型的Adapter结构上是一样的,不同的是我们要为每一种Type匹配一个layout.xml,再为每一个layout.xml匹配一个variableId用于绑定数据。先看下Adapter的全部代码。

MultiTypeAdapter

public class MultiTypeAdapter<T extends MultiTypeListItemViewModel> 
        extends RecyclerView.Adapter<MultiTypeAdapter.CustomViewHolder> {

    Context mContext;

    List<T> mData = new ArrayList<>();

    SparseIntArray mLayoutMapping;

    public MultiTypeAdapter(Context context) {
        mContext = context;
    }

    public void setData(List<T> data, SparseIntArray layoutMapping) {
        if (data != null) {
            this.mData = data;
            this.mLayoutMapping = layoutMapping;
            notifyDataSetChanged();
        }
    }

    public void addData(List<T> data) {
        if (data != null) {
            int ps = mData.size();
            mData.addAll(data);
            notifyItemRangeInserted(ps, data.size());
        }
    }

    /**
     * 设置布局文件和类型的对应关系
     *
     * @param layoutMapping 布局文件和类型的对应关系
     */
    public void setLayoutMapping(SparseIntArray layoutMapping) {
        this.mLayoutMapping = layoutMapping;
    }

    @Override
    public int getItemCount() {
        return mData.size();
    }

    @Override
    public int getItemViewType(int position) {
        return mData.get(position).mType;
    }

    @Override
    public MultiTypeAdapter.CustomViewHolder onCreateViewHolder(ViewGroup parent, int type) {
        if (mLayoutMapping != null) {
            int layoutId = mLayoutMapping.get(type);
            if (layoutId != 0) {
                ViewDataBinding binding = DataBindingUtil.inflate(
                        LayoutInflater.from(mContext), layoutId, parent, false);
                return new CustomViewHolder(binding);
            } else {
                throw new IllegalArgumentException("LayoutId is 0.");
            }
        } else {
            throw new IllegalArgumentException("LayoutMapping is null.");
        }
    }

    @Override
    public void onBindViewHolder(MultiTypeAdapter.CustomViewHolder holder, int position) {
        T itemInfo = mData.get(position);
        holder.mBinding.setVariable(itemInfo.variableId(), itemInfo);
        holder.mBinding.executePendingBindings();
    }

    public static class CustomViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding mBinding;

        public CustomViewHolder(@NonNull ViewDataBinding binding) {
            super(binding.getRoot());
            this.mBinding = binding;
        }
    }
}

仔细看下,和前面单类型Adapter大体上是一样的,主要有三个地方不一样:

  • 成员变量多了一个mLayoutMapping
  • onCreateViewHolder()方法根据type获取不同的layout.xml
  • onBindViewHolder()方法为不同的layout.xml匹配不同的variableId,绑定不同的数据

mLayoutMapping保存type类型和layout.xml的对应,因为在onCreateViewHolder()要根据type获取layout,为什么不把layout的id放在T类型的bean里面呢,因为onCreateViewHolder()的参数里只有type,没有position, 我们没有办法获取到每个位置的bean。而且多类型Adapter的type和layout的对应关系本来就不应该和bean相关,它就是独立的一组对应关系,所以用SparseIntArray保存起来。

onBindViewHolder()方法中,会为每一个layout.xml绑定一个variableId(其实就是ViewModel),这个variableId就是从每个T类型bean里取出来的,这里为什么放在bean里,就是因为这里有position参数,我们可以获取到每个位置的bean。

这里每一条消息的ViewModel是MessageViewModel,它继承自MultiTypeListItemViewModel,看下代码:

MultiTypeListItemViewModel.java

public abstract class MultiTypeListItemViewModel<T extends BaseActivity> extends ListItemViewModel<T> {

    public int mType;

    public MultiTypeListItemViewModel(T activity) {
        super(activity);
    }

    public abstract int variableId();
}

MessageViewModel .java

public class MessageViewModel extends MultiTypeListItemViewModel<ChatListActivity> {

    public ObservableInt mHeadImgResId = new ObservableInt();

    public ObservableField<String> mNickName = new ObservableField<>();

    public ObservableField<String> mMessage = new ObservableField<>();

    public MessageViewModel(ChatListActivity activity) {
        super(activity);
    }

    @Override
    public int variableId() {
        return BR.messageViewModel;
    }

    public MessageViewModel setData(int type, int headImgResId, String nickName, String lastMessage) {
        mType = type;
        mHeadImgResId.set(headImgResId);
        mNickName.set(nickName);
        mMessage.set(lastMessage);
        return this;
    }
}

父类里有一个mType成员变量,还有一个variableId()抽象方法,在子类中重写variableId()方法,返回每种type对应的variableId,这里为什么我只返回固定的一种呢?因为聊天界面左右type的layout对应的ViewModel
是一样的,然后在构建子类对象时,给mType赋值。

聊天

RxJava + Retrofit 网络请求

前面讲的三个页面都是单机游戏,数据都是本地假数据,最后来看下如果用RxJava+Retrofit进行网络请求,从服务器请求粉丝列表。

当然这个粉丝不是真的啦,只是我自己搭建的一个本地服务器,返回一段固定的json,能起到演示效果就好。

SpringMVC + Tomcat:接口返回下面这段固定的json
http://99.48.58.51:8080/springMvcDemo/testController/testFansList.do
(这里用到的头像是取自多位博客大神的头像,致敬致敬!)

{
    "code":"000",
    "desc":"请求成功",
    "content":[
        {"nickName":"张三", "lastMessage":"打南边来了个喇嘛", "imgUrl":"https://avatar.csdn.net/6/F/7/1_u011002668.jpg?1524473215"},
        {"nickName":"李四", "lastMessage":"手里提着五斤鳎蚂", "imgUrl":"https://avatar.csdn.net/B/9/9/1_u012124438.jpg?1524473242"},
        {"nickName":"王五", "lastMessage":"打北边来了一个哑巴", "imgUrl":"https://avatar.csdn.net/8/2/B/1_huachao1001.jpg?1524473255"},
        {"nickName":"赵四", "lastMessage":"腰里别着一个喇叭", "imgUrl":"https://avatar.csdn.net/5/2/A/1_u010163442.jpg?1524474105"},
        {"nickName":"刘能", "lastMessage":"提搂鳎蚂的喇嘛要拿鳎蚂去换别着喇叭的哑巴的喇叭", "imgUrl":"https://avatar.csdn.net/1/8/C/1_t12x3456.jpg?1524474115"},
        {"nickName":"大脚", "lastMessage":"别着喇叭的哑巴不愿意拿喇叭去换提搂鳎蚂的喇嘛的鳎蚂", "imgUrl":"https://avatar.csdn.net/9/3/6/1_yang_hui1986527.jpg?1524474142"},
        {"nickName":"芙蓉", "lastMessage":"提搂鳎蚂的喇嘛抡起鳎蚂就给了别着喇叭的哑巴一鳎蚂", "imgUrl":"https://avatar.csdn.net/0/D/3/1_harvic880925.jpg?1524473315"},
        {"nickName":"秀才", "lastMessage":"别着喇叭的哑巴抽出喇叭就给了提搂鳎蚂的喇嘛一喇叭", "imgUrl":"https://avatar.csdn.net/E/9/C/1_ccsutofly.jpg?1524474170"},
        {"nickName":"掌柜", "lastMessage":"也不知是提搂鳎蚂的喇嘛打了别着喇叭的哑巴", "imgUrl":"https://avatar.csdn.net/5/6/E/1_luoshengyang.jpg?1524473342"},
        {"nickName":"大嘴", "lastMessage":"还是别着喇叭的哑巴打了提搂鳎蚂的喇嘛", "imgUrl":"https://avatar.csdn.net/F/F/5/1_lmj623565791.jpg?1524474190"}
    ]
}

下面开始写代码吧

gradle

// RxJava
implementation 'io.reactivex:rxjava:1.1.0'
implementation 'io.reactivex:rxandroid:1.1.0'

// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.3.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
implementation 'com.google.code.gson:gson:2.8.2'

service

retrofit的每一个请求都需要先定义一个service

public interface FansListService {

    @POST("testFriendList.do")
    public Observable<HttpResponse<List<FansBean>>> requestFansList();
}

然后调用RetrofitManager的create方法创建service实例

FansListService mFansListService = RetrofitManager.getInstance().getDefaultRetrofit().create(FansListService.class);

至于它是怎么创建实例的先不用管,反正我们拿到了这个service实例就可以调用它的方法了。

public void requestFansList(Action0 preAction, Subscriber<List<FansBean>> subscriber) {
    mFansListSubscription = mFansListService.requestFansList()
            .map(new HttpRequestFunc<List<FansBean>>())
            .compose(RxTransformer.<List<FansBean>>applyIoTransformer())
            .doOnSubscribe(preAction)
            .subscribeOn(AndroidSchedulers.mainThread())
            .subscribe(subscriber);
}

对RxJava不熟悉的朋友可以去看我上一篇博客,这里就不讲RxJava的细节了。
https://www.jianshu.com/p/0f9ad68904cb

service的requestFansList()方法返回的是Observable对象,所以map函数把它转换成我们需要的结果数据List<FansBean>。

compose指定请求网络和结果回调的线程

preAction主要是为了在请求之前做一些前置操作,比如showLoadingView啥的

subscribe绑定了订阅者,下面就来看下这个订阅这做了哪些事情。

public void loadFansList() {
    mFansListModel.requestFansList(new Action0() {
        @Override
        public void call() {
            // showLoadingView();
        }
    }, new Subscriber<List<FansBean>>() {
        @Override
        public void onCompleted() {
            // finishLoadingView();
        }

        @Override
        public void onError(Throwable e) {
            if (e instanceof OverTimeException) {
                mActivity.showToast("");
            } else if (e instanceof ResponseErrorException) {
                mActivity.showToast("");
            } else {
                mActivity.showToast("服务器连接失败,请稍后再试~");
            }
            // finishLoadingView();
        }

        @Override
        public void onNext(List<FansBean> fansList) {
            if (fansList == null) {
                return;
            }

            List<FansViewModel> fansViewModelList = new ArrayList<>();
            for (FansBean bean : fansList) {
                FansViewModel fansViewModel = new FansViewModel(mActivity);
                fansViewModel.setData(bean.getImgUrl(), bean.getNickName(), bean.getLastMessage());
                fansViewModelList.add(fansViewModel);
            }
            mFansListViewModel.set(fansViewModelList);
        }
    });
}

onCompleted()表示事件序列的结束,所以我们需要finishLoadingView

onError()表示事件序列过程中发生异常,它和onCompleted()是互斥的,只会走其一,我们需要在这个回调中做相应的异常处理,并finishLoadingView。

onNext()就是事件的正常返回了,我们得到请求结果后,转换成ViewModel的数据,就可以更新ui了。

粉丝列表

这里说明一点,网络请求这块我省略了很多零碎的东西,因为实在没法讲,太零碎了,理不出头绪,所以只能讲一下大概的流程。

@BindingAdapter

最后有必要说一下@BindingAdapter这个东西。看了前面的代码,肯定有人会奇怪

为什么ImageView设置三个img参数就能加载图片?

<ImageView
    android:id="@+id/head_img"
    android:layout_width="84dp"
    android:layout_height="84dp"
    android:layout_marginTop="16dp"
    img:imgurl="@{userInfoViewModel.headImageUrl}"
    img:placeholder="@{@color/color_place_holder}"
    img:error="@{@color/color_place_holder}"/>

为什么RecyclerView这样设置data就可以加载出数据?

<android.support.v7.widget.RecyclerView
    android:id="@+id/friend_list_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:overScrollMode="never"
    app:data="@{friendListViewModel.mFriendListViewModel}"/>

其实背后就是@BindingAdapter这个注解在工作。

@BindingAdapter({"img:imgurl", "img:placeholder", "img:error"})
public static void loadImage(ImageView imageView, String url, Drawable holderDrawable, Drawable errorDrawable) {
    GlideApp.with(imageView.getContext())
            .load(Uri.parse(url))
            .placeholder(holderDrawable)
            .error(errorDrawable)
            .fitCenter()
            .transform(new GlideCircleTransform())
            .into(imageView);
}

我们随便写一个类,真的是随便写,因为类名不重要。然后写一个静态方法用于加载图片。
这个方法上加上一个注解@BindingAdapter({"img:imgurl", "img:placeholder", "img:error"}),注解里面有三个参数,分别表示图片url,占位图,错误图。

方法的参数是需要加载数据的控件,和注解里申明的三个参数,然后在方法内部我们可以用自己喜欢的方式加载图片,我用的是Glide,你也可以用Picasso。这样我们就定义好了一个@BindingAdapter。

在xml中使用时,我们首先要申明命名空间

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:img="http://schemas.android.com/apk/res-auto"  >

这个命名空间img就是在注解里定义的img,必须保持统一。

然后就用这个命名空间给控件赋值

<ImageView
    android:id="@+id/head_img"
    android:layout_width="84dp"
    android:layout_height="84dp"
    android:layout_marginTop="16dp"
    img:imgurl="@{userInfoViewModel.headImageUrl}"
    img:placeholder="@{@color/color_place_holder}"
    img:error="@{@color/color_place_holder}"/>

还有一点很重要的是,注解里申明了几个参数,就必须传几个参数否则会报错。

ImageView的说完了,RecyclerView自然就简单了。

public class RecyclerBinding {

    @BindingAdapter("app:data")
    public static void setRecyclerInfo(RecyclerView recyclerView , List datas){
        CommonAdapter mAdapter = (CommonAdapter) recyclerView.getAdapter();
        mAdapter.setData(datas);
    }

    @BindingAdapter({"app:data", "app:layoutMapping"})
    public static void setRecyclerInfo(RecyclerView recyclerView , List data, SparseIntArray mapping){
        MultiTypeAdapter mAdapter = (MultiTypeAdapter) recyclerView.getAdapter();
        mAdapter.setData(data, mapping);
    }
}

第一个BindingAdapter是用于单类型RecyclerView,第二个BindingAdapter是用于多类型RecyclerView。

另外还有一点需要提下,如果一个控件的某个属性没有set方法,也是需要用这种方式写的。如果是自定义View的属性,也是可以用这种方式写的。

总结

好吧,啰啰嗦嗦讲了这么多,我自己回头读一遍都觉得很凌乱,其实还有很多细节没有讲到,有些太零碎的东西我实在不知道怎么讲。总之Mvvm+RxJava+Retrofit配合起来使用确实挺不错的,习惯了写起来还挺顺的。RxJava不仅仅在和Retrofit配合时才能用,自己单独用的场景也很多。

这篇文章应该是我写的第二长的文章,第一长的是高级动画四篇。哈哈~敲的我头疼,不足之处还请谅解。

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

推荐阅读更多精彩内容