ViewModel
持有UI数据
Activity/Fragment仅展示数据和处理用户交互。
ViewModel持有UI数据。
生命周期
Activity的重建不会影响ViewModel的生命周期。
ViewModel的生命周期函数只有一个onCleared()
,当且仅当Activity代表的页面被销毁时才会调用该函数。
ViewModel引用Context
ViewModel的生命周期长于Activity,所以不应该在ViewModel中持有Activity的引用,否则将引起内存泄漏。
ViewModel不建议引入Activity,但如果ViewModel内需要Context时怎么办?可以采用以下方法之一:
1、使用Context.getApplicationContext()
;
2、使用AndroidViewModel,它是ViewModel的子类,其内部接收Application作为Context;
ViewModel的实例化
XXXViewModel mXXXViewModel = new ViewModelProvider(this).get(XXXViewModel.class);
ViewModel与onSaveInstanceState()的区别
-
数据差异
onSaveInstanceState()只能保存少量,可序列化的UI数据,不能保存Bitmap这样的大数据。
ViewModel没有这样的限制。 -
数据持久化
onSaveInstanceState()可以在以下两种情况下保存少量 的UI 数据:
① 应用的进程在后台的时候由于内存限制而被终止。
② 配置更改。
ViewModel 只能在配置更改的销毁情况下保留数据,而不能在被终止的进程中保留。
LiveData
用途
LiveData可以理解为一个数据的容器。它将数据包装起来,使数据变成一个被观察者,当数据发生变化时,观察者能够获得通知。
ViewModel持有UI数据,Activity/Fragment负责展示数据,如果UI数据发生变化,就由LiveData通知Activity/Fragment刷新数据。所以LiveData通常放在ViewModel中使用。
基本使用
LiveData是一个抽象类,不能直接使用。通常我们使用的是它的子类MutableLiveData。
通过LiveData.observe()方法对LiveData所包装的数据进行观察。反过来,当我们希望修改LiveData所包装的数据的时候,可以通过LiveData.postValue()/LiveData.setValue()方法来完成。postValue()在非UI线程中调用,setValue()在UI线程中调用。
通知更新
LiveData可以感知页面的生命周期,只有当页面处于活跃状态(Lifecycle.State.STARTED或Lifecycle.State.RESUMED)才会收到LiveData的通知,若页面被销毁(Lifecycle.State.DESTROYED),那么LiveData会自动清除与页面的关联,从而避免内存泄漏。
通常,LiveData 仅在数据发生更改时才发送更新,并且仅发送给活跃观察者。此行为的一种例外情况是,观察者从非活跃状态更改为活跃状态时也会收到更新。此外,如果观察者第二次从非活跃状态更改为活跃状态,则只有在自上次变为活跃状态以来值发生了更改时,它才会收到更新。
LiveData.observeForever()使用方式与observe差不多,区别是当数据发生变化时无论页面处于什么状态下都能收到通知。因此用完后一定要调用removeObserver(),移除观察者,避免内存泄漏。
ViewModel+LiveData实现Fragment之间的通信
public class OneFragment extends Fragment {
public void onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
/*
关键在于ViewModelProvider的构造函数传入的是getActivity()而不是Fragment.this,
这样才能保证每个Fragment得到的是同一个ViewModel,从而共享LiveData
*/
XXXViewModel mXXXViewModel = new ViewModelProvider(getActivity()).get(XXXViewModel.class);
}
}
//TwoFragment与OneFragment类似
小结
LiveData的本质就是观察者模式+感知生命周期。
DataBinding
简单使用
- 启动DataBinding
android {
……
dataBinding {
enabled = true;
}
}
- 标记布局文件
在布局文件的根目录外增加<layout>
标签。这样做的目的是告诉DataBinding库生成该布局文件对应的Binding类。
<layout xmlns:android="http://schemas.android.com/apk/res/android">
/*
以下是实际布局
……
*/
</layout>
- 定义布局变量
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name = "变量名"
type = "类全名"/>
/*
或者使用<import>标签引入类
<import type = "类全名"/>
<variable
name = "变量名"
type = "类名称"/>
*/
</data>
/*
以下是实际布局
……
*/
</layout>
- 获取Binding类
//该方法给Activity设置布局文件的同时,返回Binding类。
XXXBinding mXXXBinding = DataBindingUtil.setContentView(this, R.layout.xxx);
-
布局变量的赋值
Binding提供了2种给布局变量赋值的方法:
①通用方法:XXXBinding.setVariable(BR.变量名, 变量);
②针对特定布局变量的赋值方法:XXXBinding.set变量名(变量)
-
布局表达式
布局表达式的格式:@{}
。
比如:@{布局变量.字段}
、@{方法调用的表达式}
<data>
<import type = "xxx.xxx.TestUtil"/>
<variable
name = "book"
type = "xxx.xxx.Book"/>
</data>
<!-- 在布局中引用静态类-->
<TextView
android:text="@{TestUtil.getText()}"/>
<TextView
android:text="@{book.name}"/>
布局表达式远不止这些用法,详见:Data Binding 详解(二)-布局和绑定表达式
- Activity最终样子
public class TestActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
XXXBinding mXXXBinding = DataBindingUtil.setContentView(this, R.layout.xxx);
Book book = new Book();
book.name = "Jetpack应用指南";
mXXXBinding.setBook(book);
}
}
事件绑定
DataBinding支持用布局表达式来处理View的事件响应。
具体做法:在布局文件中给View的事件属性赋值布局表达式。
这样相当于用布局表达式来实现对应监听器的回调。这种做法被称为事件绑定。
事件属性与监听器的对应关系
事件属性名字取决于监听器方法名字。例如 View.OnClickListener 有 onClick()
方法,View.OnLongClickListener 有 onLongClick()
的方法,因此事件的属性是 android:onClick
、android:onLongClick
。
对于 click 事件,为了避免多种 click 事件的冲突,Google也定义了一些专门的事件处理,比如:
Class | 设置监听器的方法 | 绑定时的属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
事件绑定的布局表达式有2种:引用方法和绑定监听器。
引用方法
使用布局变量的方法来响应事件。该方法要求参数和返回值必须与监听器的参数和返回值相匹配。如果参数或者返回值不匹配则会在编译时报错。
public class EventHandler {
public void onClickHandle(View view) {
System.out.println("按钮被点击了");
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="eventHandler"
type="xxx.xxx.EventHandler" />
</data>
...
<Button
...
android:onClick="@{eventHandler::onClickHandle}"
... />
...
</layout>
使用“引用方法”时,生成的监听器会封装布局变量的方法调用。该监听器对象是在布局变量被设置的时候创建并赋值的。如果布局变量为null,则不会创建该监听器。
/*
“引用方法”的监听器创建原理如下伪代码所示
伪代码是在运行时运行的。
*/
xxxBinding.setEventHandler(EventHandler eventHandler) {
if(eventHandler != null) {
button.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
eventHandler.onClickHandle(view);
}
});
}
}
绑定监听器
在布局文件中使用lambda来响应事件。该方法只要求返回值与监听器的预期返回值匹配即可。
public class Tester {
public boolean testLongClick() {
return false;
}
}
<Button
...
android:onClick="@{()->tester.testLongClick()}"
... />
绑定监听器允许携带自定义的参数。
public class Tester {
public boolean testLongClick(View v, String info) {
Toast.makeText(v.getContext(), info, Toast.LENGTH_LONG).show();
}
}
<Button
...
android:onClick="@{(view)->tester.testLongClick(view, '你好')}"
... />
“绑定监听器”在编译时会自动创建必要的监听器并为它注册事件(监听器是一开始就创建好了,等到触发时才会判断布局变量是否为空,为空则不执行任何操作)
/*
“绑定监听器”的监听器创建原理如下伪代码所示
伪代码是在编译时运行的。
*/
button.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
if(tester != null) {
tester.testLongClick(view, "你好");
}
}
});
三元表达式
如果需要使用带有谓词的表达式(例如,三元表达式) ,可以使用监听器相匹配的返回值类型作为表达式,比如 onCLick 属性使用 void,onLongClick 属性使用 Boolean。
android:onClick="@{(view)->view.isEnabled()?activity.showSign(view, user):void}"
android:onLongClick="@{(v)->v.isEnabled()?activity.showSign(user):false}"
二级页面的绑定
我们将Activity/Fragment直接引用的页面称为一级页面,在一级页面中通过标签引用的页面称为二级页面。
如何将布局变量从一级页面传递给二级页面呢?
在一级布局中定义了布局变量book
后,不仅可以再一级布局中接收并使用该变量,而且该变量也成为了命名空间xmlns:app
的一个属性。
该属性的用途就是将布局变量book
传给二级布局。
//一级页面
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name = "book"
type = "xxx.xxx.Book"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_content"
app:book="@{book}">
</LinearLayout>
</layout>
//二级页面
/**
在二级页面layout_content中,需要定义一个与一级页面相同的布局变量,
用来接收传递过来的数据。收到book变量后即可使用该变量了。
*/
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name = "book"
type = "xxx.xxx.Book"/>
</data>
<TextView
……
android:text="@{book.name}"/>
</layout>
BindingAdapter
绑定适配器(BindingAdapter)就是把布局中的属性表达式转换成对应的方法调用以设置值。
所谓的设置值分为2种:
①设置属性值,比如调用 setText() 方法
②设置事件侦听器,比如调用 setOnClickListener() 方法。
还允许你自定义设置值的调用方法,提供你自己的绑定逻辑。
DataBinding库中的BindingAdapter
DataBinding库中提供了许多XXXBindingAdapter类,这些类使得android原生控件支持属性表达式。
//DataBinding库下ViewBindingAdapter的部分源码
public class ViewBindingAdapter {
@BindingAdapter({
"android:padding"})
public static void setPadding(View view, float paddingFloat) {
final int padding = pixelsToDimensionPixelSize(paddingFloat);
view.setPadding(padding, padding, padding, padding);
}
}