MVVM的基本概念
项目源码地址: https://github.com/corffen/MVVMDemo
1.什么是MVVM?
它是model-view-viewmodle的缩写
为何要用MVVM?
需求不断地提出,应用一天比一天复杂,fragment和activity开始膨胀,逐渐变得难以理解和扩展,这个时候控制器层需要做功能拆分.
MVVM能能够很好的把控制器里的臃肿代码放入到布局文件里,很容易地看出哪些是动态界面,同时抽出部分动态控制器代码放入viewModel中.
MVVM的基本使用
1.在build.gradle文件中添加
`
dataBinding {
enabled = true
}
`
2.在布局文件中
将根目录结构换成layout
如:
`
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:gravity="center"
android:text="基本使用"
android:id="@+id/tv_title"
android:layout_gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
`
3.在Activity或者Fragment中初始化DataBinding
`
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityBasicBinding activityBasicBinding = DataBindingUtil.setContentView(this, R.layout.activity_basic);
activityBasicBinding.tvTitle.setText("我不需要findviewbyid");
}
`
运行效果:
[图片上传失败...(image-a9e259-1524712938506)]
可以看到我们并没有初始化TextView,直接使用DataBinding的打点调用控件就行了.
上面的ActivityBasicBinding这个东西其实是根据我们的布局文件名字自动生成的.
这样我们不需要手动的去写一大堆的findviewbyid了,当然它的功能远不止这些.
我们想要的效果是view可以跟数据绑定起来,当数据变化的时候,view会自己刷新UI
4.view绑定数据
假设我们需要的是TextView显示的内容是一个bean的属性,比如User类中的一个name
那么可以向下面这样去写
首先在布局文件中添加
data标签
`
<data>
<variable
name="user"
type="com.corffen.mvvmdemo.User"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_view_bind_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:text="@{user.name}"/>
`
然后使用@{}语法糖去设置属性
最后在Activity中将我们的bean类设置给DataBinding
`
final ActivityViewBindDataBinding activityViewBindDataBinding = DataBindingUtil.setContentView(this, R.layout
.activity_view_bind_data);
mUser = new User("绑定数据啊");
activityViewBindDataBinding.setUser(mUser);
`
运行效果如图
我们并没有调用textview的setText函数,就可以直接显示数据了.
现在我们点击"改变数据按钮",更改bean的属性
添加如下:
`
activityViewBindDataBinding.btnChangeData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
count++;
mUser.setName("我改变了数据" + count);
}
});
`
发现点击按钮之后,textview并没有更改数据.
这是因为我们在点击按钮时,数据bean类,已经改变了,但是view并不知道,所以
我们在点击事件里再添加一行代码
`
mUser.setName("我改变了数据" + count);
activityViewBindDataBinding.setUser(mUser);
`
再次运行如图:
[图片上传失败...(image-14fdc3-1524712938506)]
也就是说我们在点击事件中,重写给DataBinding设置了数据bean类,当数据变化时,需要通知view,view就可以正确的显示我们想要的数据了.
那么这个setUser方法到底干什么了.点击源码进去可以看到
`
public void setUser(@Nullable com.corffen.mvvmdemo.User User) {
this.mUser = User;
synchronized(this) {
mDirtyFlags |= 0x1L;
}
notifyPropertyChanged(BR.user);
super.requestRebind();
}
`
调用了一个
notifyPropertyChanged(BR.user);这样的方法去刷新数据.那我们总不能再数据每次改变的时候,都去调用一次 activityViewBindDataBinding.setUser(mUser);这样的方法,这也太low了.
BaseObservable的使用
首先让我们的bean类继承自BaseObservable,然后在get方法上添加一个@Bindable
然后在set方法中调用notifyChange(); 修改如下:
`
public class User2 extends BaseObservable {
private String name;
public User2(String name) {
this.name = name;
}
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyChange();
}
}
`
布局文件可以不用动,然后我们把点击事件中的DataBinding.setuser方法屏蔽掉,运行结果:
[图片上传失败...(image-228b6e-1524712938506)]
实现了与上面的一样的效果.
点击事件的实现
首先在布局文件中添加
`
<Button
android:id="@+id/btn_change_data3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->user3.clickMe()}"
android:text="@{user3.btnContent}"/>
`
上面text使用了User中btnContent属性,然后还多了一个onClick
这是什么鬼?
这是给Button设置了一个点击事件,具体的实现逻辑在User3中,如下:
`
public void clickMe() {
count++;
setBtnContent("点我啊" + count);
}
然后在Activity中给DataBinding设置Data:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityClickBinding activityClickBinding = DataBindingUtil.setContentView(this, R.layout.activity_click);
activityClickBinding.setUser3(new User3("我是点击事件", "点我啊"));
}
`
运行效果:
可以看到Activity中的代码基本不用怎么写了,看起来很清爽,因为业务逻辑交给布局和model层去处理了,但是有一个问题,就是我们在model层做了逻辑处理,因为view和data交互的时候,data不可避免的要关心显示问题,这就违背了单一原则,为了解决这个问题,我们想起了一句名言
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
ViewModel
我们希望的是view作为界面的显示,而bean类只作为基本的数据,不去参与具体的逻辑处理.
如下代码:
`
public class User4 {
private String btnContent;
public String getBtnContent() {
return btnContent;
}
public void setBtnContent(String btnContent) {
this.btnContent = btnContent;
}
public User4(String btnContent) {
this.btnContent = btnContent;
}
}
`
所以我们还需要创建一个中间层来处理相应的业务逻辑.
`
public class ClickViewModel extends BaseObservable {
private User4 mUser4;
private int count;
public ClickViewModel(User4 user4) {
mUser4 = user4;
count = 0;
}
@Bindable
public String getBtnContent() {
return mUser4.getBtnContent();
}
public void setBtnContent(String content) {
mUser4.setBtnContent(content);
notifyChange();
}
public void clickMe() {
count++;
setBtnContent("点我啊" + count);
}
}
`
viewModel持有User4类,因为我们需要在数据变化的时候通知界面去刷新UI,所以我们的viewModel继承BaseObservable,并给需要的BtnContent设置了@Bindable
然后看一下布局
`
<data>
<variable
name="clickViewModel"
type="com.corffen.mvvmdemo.ClickViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center"
android:text="我是ClickViewModel界面"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->clickViewModel.clickMe()}"
android:text="@{clickViewModel.btnContent}"/>
</LinearLayout>
</layout>
注意到android:text="@{clickViewModel.btnContent}",而我们的viewModel并没有提供btnContent这个属性,实际上他是调用了getBtnContent()也就是说他是方法的简写.另外值得一提的是我们在Activity中给DataBinding设置viewModel时,使用的方法 如下:
activityClickWithViewModelBinding.setClickViewModel(new ClickViewModel(new User4("点我啊!")));
activityClickWithViewModelBinding.executePendingBindings();
`
这里的setClickViewModel其实是布局文件中data标签下的name属性来的.
在数据变化时,调用notifyChange,在读取数据的方法上加上@Bindable注解.
多个property的ViewModel
在上一个例子当中,我们要的数据是User4中的一个属性,我们做的地方首先是viewModel继承了BaseObservable,然后提供了get(需要@Bindable注解)和set(需要notifyChange)方法,这样看起来好像有些麻烦,而且有时候我们自定义的ViewModel类可能继承了别的类,比如我们在封装的时候,ViewModel可能已经继承了BaseViewModel,这样我们就不能再继承BaseObservable了,这样数据变化了,UI便不能更新了.
所以系统提供了Observable系列的封装类,供我们去使用.
比如我们有多个属性
`
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->multiPropertyViewModel.click1()}"
android:text="@{multiPropertyViewModel.content1}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->multiPropertyViewModel.click2()}"
android:text="@{multiPropertyViewModel.content2}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->multiPropertyViewModel.click3()}"
android:text="@{multiPropertyViewModel.content3}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="20dp"
android:gravity="center"
android:onClick="@{() ->multiPropertyViewModel.click4()}"
android:text="@{multiPropertyViewModel.content4 }"/>
我们的viewModel类可以这样去写
public class MultiPropertyViewModel {
private int count1 = 0, count2 = 0, count3 = 0, count4 = 0;
private ObservableField<String> mContent1 = new ObservableField<>("第一个按钮");
private ObservableField<String> mContent2 = new ObservableField<>("第二个按钮");
private ObservableField<String> mContent3 = new ObservableField<>("第三个按钮");
private ObservableField<String> mContent4 = new ObservableField<>("第四个按钮");
public MultiPropertyViewModel() {
}
public ObservableField<String> getContent1() {
return mContent1;
}
public ObservableField<String> getContent2() {
return mContent2;
}
public ObservableField<String> getContent3() {
return mContent3;
}
public ObservableField<String> getContent4() {
return mContent4;
}
public void click1() {
count1++;
mContent1.set("第一个按钮点击了" + count1);
}
public void click2() {
count2++;
mContent2.set("第二个按钮点击了" + count2);
}
public void click3() {
count3++;
mContent3.set("第三个按钮点击了" + count3);
}
public void click4() {
count4++;
mContent4.set("第四个按钮点击了" + count4);
}
}
`
不需要继承自BaseObservable,然后定义4个ObservableField<String>,提供get方法
这里注意一个问题是,get方法一定要返回ObservableField<String>而不是String类的,否则看不到数据的更新,然后点击按钮的时候,我们更改了数据,就调用
ObservableField<String>的set方法,运行效果如下图:
BindingAdapter的使用
经常有一个这样的场景,比如我有一个TextView,用来显示一段文字,每当数据变化的时候,我都需要调用此控件的setText方法,我有一个ImageView,在不同的状态时,需要显示不同的图片,就去调用类似setDrawable这样的方法,而对于加载网络图片,只需要知道url就行了.然后使用开源框架,去加载图片.
对于这些常见的场景,我们能不能只改变数据,让控件自动去正确的显示我们需要的数据.
我们先来看一下下面的代码
@BindingAdapter("imageUrl")
public static void setImageUrl(ImageView imageView, String url) {
Context context = imageView.getContext();
Glide.with(context).load(url).
into(imageView);
}
这是一个使用Glide加载图片的方法,方法上面有一个注解BindingAdapter,还有一个参数imageUrl.
这个参数其实是一个属性的意思,就是说我们给ImageView设置了一个自定义个属性imageURL,当我们给ImageView设置这个属性,并给其值设置一个URL,那么就会调用这个方法去加载图片.
在布局文件中我们就可以使用
<ImageView
android:id="@+id/iv_net"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{viewModel.imageUrl}" />
然后我们只需要关注viewModel.imageUrl这个值的变化就可以更改ImageView显示的图片了.上面的app:imageUrl 可以理解为,我们自定义了一个控件ImageView,然后写了一个imageUrl的自定义属性,它的值是String类型的,然后给这个属性设置值时,就去执行相应的@BindingAdapter的方法了.
其实我们在使用TextView时,会见到这样的一个写法
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.textContent}" />
当我们的viewModel.textContent值变化时,就会显示对应的text.假设我们有一个需求就是让我们的TextView每次都显示大写的内容.改怎么做呢?
这里的text,其实是系统提供给我们的一个BindingAdapter写法.我们可以仿照系统提供的方法,然后自定义一个@BindingAdapter方法
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
//下面这句代码,就是我们加进去的
CharSequence upperText = ((String) text).toUpperCase();
view.setText(upperText);
}
我们在设置内容时,它的值就会变成大写的了.运行效果如图:
源码在GitHub中可以查看
RecyclerView的使用
Rv是使用频率非常高的一个控件了,我们在使用它时,无非是当数据源变化时写Adapter去适配其显示的方式.对Rv来说也就是说,给我们一个数据源,我就知道如何去显示自己的内容了.
所以利用上一节的内容,可以写以下的一个方法.
@BindingAdapter("adapter")
public static void setAdapterForRv(RecyclerView rv, List<String> datas) {
RvAdatper adapter = (RvAdatper) rv.getAdapter();
if (adapter != null) {
adapter.clearItems();
adapter.addItems(datas);
}
}
这里说一下这个方法,可以写很多的重载方法,当给不同的数据源时,Rv去寻找对应的方法.比如我还有一个以下的方法
@BindingAdapter("adapter")
public static void setAdapterForRv(RecyclerView rv, List<Bean> datas) {
RvAdatper adapter = (RvAdatper) rv.getAdapter();
if (adapter != null) {
adapter.clearItems();
adapter.addItems(datas);
}
}
上面给adapter提供的两个方法如下:
public void addItems(List<String> datas) {
this.mDatas.addAll(datas);
notifyDataSetChanged();
}
public void clearItems() {
mDatas.clear();
}
对了这种@BindingAdapter的方法一定需要是静态的,而且它放在任何类都可以,所以为了统一,我们经常会将这样的方法,统一放在一个类里.比如BindingUtils类.
写好上面的方法后,在布局中就可以像下面这样写
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{viewModel.mStringObservableArrayList}"
tools:listitem="@layout/item_string_rv">
</android.support.v7.widget.RecyclerView>
上面的adapter就是我们给Rv自定义的属性,它的值是一个List<String>,它有点特殊,因为我们需要观察数据变化时去刷新UI,所以我们用了一个ObservableArrayList,这个跟上面讲的ObservableField系列没啥不同,它只是用来存放List类型的数据而已.
tools:listitem="@layout/item_string_rv" 这行代码是用来告诉IDE,我们在as中预览到我们的item而已.
所以在写好常规的adapter中后,我们只需要关注数据源的变化就可以了.
public class RvViewModel extends BaseObservable {
public final ObservableArrayList<String> mStringObservableArrayList = new
ObservableArrayList<>();
private int count = 0;
public RvViewModel() {
}
public void setData() {
for (int i = 0; i < 9; i++) {
mStringObservableArrayList.add("我是item" + i);
}
}
public void addData() {
count++;
mStringObservableArrayList.add("我是添加的数据" + count);
}
public void clearData() {
mStringObservableArrayList.clear();
}
public void clearOneData() {
if (mStringObservableArrayList.size() > 0) {
mStringObservableArrayList.remove(mStringObservableArrayList.size() - 1);
}
}
}
在给Activity中设置Rv的使用如下啊:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityRvBinding = DataBindingUtil.setContentView(this, R.layout
.activity_rv);
mRvViewModel = new RvViewModel();
activityRvBinding.setViewModel(mRvViewModel);
//给数据源初始化数据
mRvViewModel.setData();
setAdapter();
}
private void setAdapter() {
LinearLayoutManager manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL,
false);
//注意这里并没有在构造中设置数据源
mRvAdatper = new RvAdatper(new ArrayList<String>());
activityRvBinding.rv.setLayoutManager(manager);
activityRvBinding.rv.setAdapter(mRvAdatper);
}
然后运行效果如图:
LiveData和MultiLiveData
https://www.jianshu.com/p/13a855ceaf2b
这里有一篇链接,讲的挺好的.
LiveData 定义
假设有这样的一个场景,我在Activity中的onCreate方法中执行了一段异步加载数据的逻辑,
当加载完数据之后,需要刷新我的界面.可是我在加载数据尚未完成的时候,屏幕旋转,导致我们重新加载数据,异步线程加载的数据要在一个已经销毁的控件上显示会导致错误.这样的解决方式,我们有多种,而我们要讲的主题是LiveData.
简单地说,LiveData是一个数据持有类。它具有以下特点:
数据可以被观察者订阅;
能够感知组件(Fragment、Activity、Service)的生命周期;
只有在组件出于激活状态(STARTED、RESUMED)才会通知观察者有数据更新;
LiveData的优点
从LiveData具有的特点,我们就能联想到它能够解决我们遇到的什么问题。LiveData具有以下优点:
能够保证数据和UI统一
这个和LiveData采用了观察者模式有关,LiveData是被观察者,当数据有变化时会通知观察者(UI)。减少内存泄漏
这是因为LiveData能够感知到组件的生命周期,当组件处于DESTROYED状态时,观察者对象会被清除掉。当Activity停止时不会引起崩溃
这是因为组件处于非激活状态时,不会收到LiveData中数据变化的通知。不需要额外的手动处理来响应生命周期的变化
这一点同样是因为LiveData能够感知组件的生命周期,所以就完全不需要在代码中告诉LiveData组件的生命周期状态。组件和数据相关的内容能实时更新
组件在前台的时候能够实时收到数据改变的通知,这是可以理解的。当组件从后台到前台来时,LiveData能够将最新的数据通知组件,这两点就保证了组件中和数据相关的内容能够实时更新。针对configuration change时,不需要额外的处理来保存数据
我们知道,当你把数据存储在组件中时,当configuration change(比如语言、屏幕方向变化)时,组件会被recreate,然而系统并不能保证你的数据能够被恢复的。当我们采用LiveData保存数据时,因为数据和组件分离了。当组件被recreate,数据还是存在LiveData中,并不会被销毁。资源共享
通过继承LiveData类,然后将该类定义成单例模式,在该类封装监听一些系统属性变化,然后通知LiveData的观察者,这个在继承LiveData中会看到具体的例子。
3.11.3 LiveData的基本使用
首先在项目工程里添加依赖
`
// view model
implementation "android.arch.lifecycle:extensions:1.1.0"
annotationProcessor "android.arch.lifecycle:compiler:1.1.0"
`
然后在根目录工程里添加
allprojects {
repositories {
google()
//把这行添加进去
maven { url 'https://maven.google.com' }
jcenter()
}
}
最后执行使用流程.假设我现在要实现下面这样的一个需求.
最上面的TextView用于显示文本,下面的改变数据按钮,点击时改变文本的显示.
最下面的列表是RecyclerView,点击上面的改变数据集合,RecyclerView就显示对应的数据源.
当点击item时,最上面的Textview就显示点击的item的内容.
具体实现细节
这里只讲列表数据的关联变化,按钮点击的变化比较简单,可以看看源码就明白了.
首先我们在ViewModel中定义一个
public final ObservableArrayList<String> mStringObservableArrayList = new
ObservableArrayList<>();
用于提供列表的数据.
然后定义一个系统提供的继承自LiveData的MutableLiveData.
它持有的数据类型为List<String>
private MutableLiveData<List<String>> mSourceDatas;
提供一个get方法
public MutableLiveData<List<String>> getSoureceDatas() {
if (mSoureceDatas == null) {
mSoureceDatas = new MutableLiveData<>();
}
return mSoureceDatas;
}
在Activity中将LiveData与我们的组件(Activity)关联起来.
mLiveDataViewModel.getSoureceDatas().observe(this, new Observer<List<String>>() {
@Override
public void onChanged(@Nullable List<String> strings) {
mLiveDataViewModel.addTitleToList(strings);
}
});
这个observe方法的第一个参数this,指的是我们的组件Activity,第二个参数是可观察的数据.
这个方法的意思是当我们观察的数据源改变之后,就会回调这个方法去通知给LiveData.
然后我们实现addTitleToList这个方法,
public void addTitleToList(List<String> titles) {
mStringObservableArrayList.clear();
mStringObservableArrayList.addAll(titles);
}
其实就是修改我们的列表的数据源.
因为进来列表要显示初始的数据,所以给mSoureceDatas设置初始数据
public void setData() {
List<String> titls = new ArrayList<>();
for (int i = 0; i < 10; i++) {
titls.add("我是item" + i);
}
mSoureceDatas.setValue(titls);
}
然后给更改数据源按钮提供一个点击方法.
public void changeDatas() {
dataCount++;
mStringObservableArrayList.clear();
for (int i = 10 * dataCount; i < 10 + dataCount * 10; i++) {
mStringObservableArrayList.add("我是改变后的item" + i);
}
}
首先清空之前的数据,,然后添加修改后的数据.
根据上面说的,当数据源改变的时候,就会通知LiveData,去执行observe方法,而Observable系列的数据是跟控件绑定的,它会自己刷新UI的.
然后在实现点击item时,显示item的值.
首先在Adapter设置一个点击item的回调监听,这个就不讲了.
然后
mRvAdatper.setOnclickListener(new RvAdatper.OnItemClickListener() {
@Override
public void onItemClick(int position) {
mLiveDataViewModel.updataContent(position);
}
});
当点击item的时候就去执行mLiveDataViewModel.updataContent(position);
它是干嘛的呢?
public void updataContent(int position) {
content.set(mStringObservableArrayList.get(position));
}
content是TextView的数据观察
public final ObservableField<String> content = new ObservableField<>("我是初始值");
当点击item时,去给content设置item上的内容就可以了.
这样功能就实现了.我们现在来回顾一下,发现,数据与UI是完全隔离的,在ViewModel中,只关心数据的变化,而UI的各种交互点击在布局文件中声明好就行了.这为我们省去了很多”垃圾代码”.最后使用LiveData又帮我们做了数据与组件生命周期的优化工作.
这些不就是mvvm的魅力吗?