废话
说起现在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,类似一个社交软件,有四个页面:用户信息、好友列表、聊天记录、粉丝列表
- 用户信息:这个页面很简单,主要是演示常规页面数据绑定的写法
- 好友列表:这个页面主要演示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配合时才能用,自己单独用的场景也很多。
这篇文章应该是我写的第二长的文章,第一长的是高级动画四篇。哈哈~敲的我头疼,不足之处还请谅解。