这篇以前在简书写的blog,由于已经不会在简书写东西了,就搬过来这边
在项目中使用到了DataBinding,深感它的优秀,于是进行分享。
DataBinding,数据绑定,可以直接在xml中绑定数据并实现一些处理逻辑,实时动态刷新数据。它的功能强大,可以节省很多手写的代码,而且性能也很好。
上面说了它的性能很好,因为它0反射,而且性能比直接findViewById要高。可以查看DataBinding的源码:
还有它生成的实体类:
一个Activity会有一个Window对象,而一个Window对象也有一个DecorView。DecorView是一个ViewGroup,布局文件都是通过inflate转化为view,加入到DecorView中,可以说DecorView是最大的根布局,而这个android.R.id.content正是它的id。DataBinding通过获取这个根布局,然后通过for循环将里面的控件一个个return出去,然后在生成的实体类再一个个获取。这样子的效率比直接findViewByid要效率的多,因为每次findViewByid都需要进行一次for循环在ViewGroup里面来寻找指定id名的控件。
目前AS对DataBinding在xml编写时还是不太友善的,代码自动补全功能做得还是有点差,这个有待Google日后的改善。
使用方法很简单,Gradle 1.5 alpha 及以上自带支持 DataBinding,仅在app模块的build.gradle中加上几行代码就可以使用了:
android {
...
dataBinding {
enabled = true
}
...
}
将一个layout变成DataBinding的layout也很简单的:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
//原来的layout 代码
layout>
当我们将layout改造完成后,DataBinding就会自动生成对应的编译文件了。假如没有,可以make project一下。
而编译文件的命名一般都是通过xml的文件名生成,如activity_main.xml,则会生成ActivityMainBinding,item_list_name则会生成ItemListNameBinding。
上面有提到过的,DataBinding可以实时动态刷新数据。这是通过继承DataBinding的BaseObservable来做到的。
public class InfoBean extends BaseObservable{
private String unit;
private String rental;
private List list;
public InfoBean(String unit, String rental,List list) {
this.unit = unit;
this.rental = rental;
this.list = list;
}
@Bindable
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
notifyPropertyChanged(BR.unit);
}
public String getRental() {
return rental;
}
public void setRental(String rental) {
this.rental = rental;
}
@Bindable
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
notifyPropertyChanged(BR.list);
}
}
BR.java是类似R.java的资源文件,是 Binding Resources 的缩写,由框架自动生成。
注意,BR 中的 id 生成的依据是 @Bindable 修饰的方法名 getXXX(),而非方法体的内容。当在 getXXX() 方法前加 @Bindable 之后,BR.java中会自动生成常量BR.XXX。如果觉得自动生成速度不够快,也可以试下make project一下。
DataBinding还提供了其他已经实现好的BaseObservable子类,包括 ObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, 和ObservableParcelable。
它们在xml中,使用方法和普通的String,int一样,只是会自动刷新,但在java中访问则会相对麻烦,因为它是通过get/set方法来进行获取和修改的。这里使用ObservableBoolean来举个例子吧:
public ObservableBoolean Show = new ObservableBoolean(true);
...
Boolean flag = Show.get();
Show.set(false);
上面有提到过,DataBinding是可以在xml里面写一些处理逻辑的。我们将一个layout文件改成DataBinding模式后,通过DataBinding的data标签,我们可以很轻松给控件绑定文本等属性和绑定点击方法的。我们来看下面这一段的xml逻辑吧:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="presenter"
type="com.fritz.demo.DemoActivity.DemoPresenter" />
<variable
name="item"
type="fritz.databinding.com.Bean.InfoBean" />
</data>
...........//原来的布局lyaout
</layout>
我们先来解析上面的代码吧,我们通过data标签来给DataBinding来设置需要绑定的数据,如果是使用系统自带的静态变量就需要像上面那样导入
type="android.view.View" />
如果需要绑定给TextView之类的控件文本,就需要给它一个绑定数据源,这里就是上面的InfoBean,当然你还得给这个数据源起一个名字(就像findViewById),同时也得写清楚这个数据源的所在路径,DataBinding才能找到它,完成绑定操作。
上面是绑定数据源,除了数据源可以绑定,方法也可以做绑定的,正如上面的HomePresenter 。当然了,也得写清楚这个指定方法对象的所在路径和名字。好了,现在看下我们的layout主体应该要怎么玩耍吧:
"match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="@dimen/activity_vertical_margin"
tools:context="com.fritz.demo.DemoActivity">
上面的xml代码逻辑应该一眼就看明白一部分了吧。当item.unit不为null时就使用它,否则就使用item.rental。同时id命名为txtTitle的这个TextView是否显示依据item.list.size这个集合的大小是否大于0。
不仅如此,我们还可以看到下方的ImageView可以得到id命名为txtTitle这个TextView的显示属性的赋值,如果这些逻辑我们都放在Java代码去写,你猜需要写多少行Java代码才能实现上面的功能呢?
而且DataBinding在绑定数据时,发现空值的对象都会给它一个初始值的。如果String为null就直接给它一个”“字符,int,float,long之类的就是一个0了。
这就帮助我们有效避免了空指针。
但是需要注意的数组或集合越界的问题,xml里面目前是没有方法可以判定数组或集合的size。
(这里其实还有一点需要注意的,如果要通过控件的id来获取控件的visibility属性,控件的id命名不能有符号的,否则DataBinding是无法识别的)
这里贴下DataBinding在xml中支持的计算表达式:
算术 + - / * %
字符串合并 +
逻辑 && ||
二元 & | ^
一元 + - ! ~
移位 >> >>> <<
比较 == > < >= <=
Instanceof
Grouping ()
文字 - character, String, numeric, null
Cast
方法调用
Field 访问
Array 访问 []
三元 ?:
尚且不支持this, super, new, 以及显示的泛型调用
估计不少人会疑惑上面的3个监听方法吧。现在来说明下DataBinding是如何绑定执行方法吧:
DataBinding在xml里面进行方法绑定有两个方法:
一种是使用lambda 表达式,如()->presenter.onItemClick(item)
一种方法引用,如presenter.onClick或presenter.onTextChanged
那么它们有什么区别呢?
其实从我们上面的代码逻辑其实就可以猜出来了,那就是方法引用不能传递引用方法以外的参数,而lambda 表达式可以传递更多的数据给指定方法。
它们的使用规则如下:
1).方法返回值确保一致:方法引用必须保证绑定的两个方法的返回值为一致,如View的onClick方法没有返回值,那么我们创建的绑定方法onClick也不能有任何返回值。不过,lambda 表达式就没有这么严谨,在绑定的View方法没有返回值的情况下,我们创建的绑定方法有无返回值都是ok的。但是如果绑定的View方法有返回值,使用lambda 表达式绑定方法时也必须保证方法的返回值是一致的。
2).方法参数需要匹配:方法引用必须保证绑定的两个方法的里面的参数都为一致。如EditText的onTextChanged方法,我们进行绑定时必须确保绑定方法的参数和EditText的onTextChanged一致。而使用lambda 表达式绑定方法的参数也必须有同样的参数,只是在这些基础上,我们使用lambda 表达式可以添加更多的参数进行传递。
目前这个我只在EditText里面使用到。很简单,在要使用双向绑定的地方,使用 “@={}” 即可。
在InfoBean里面添加一些新的属性吧:
private String price;
public InfoBean(String unit, String rental, String price,List list) {
this.unit = unit;
this.rental = rental;
this.price = price;
this.list = list;
}
@Bindable
public String getPrice(){
return price;
}
public void setPrice(String price) {
this.price = price;
notifyPropertyChanged(BR.price);
}
然后xml添加新的代码:
"match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@={item.price}"/>
这样子就可以了愉快使用双向绑定了。
关于双向绑定我另开了一篇blog:DataBinding的双向绑定,对双向绑定有兴趣的可以去看看。
Include这个布局标签在DataBinding布局里面使用有点特殊, 因为它需要我们传递绑定的方法和数据对象。比如我们有以下的include布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="presenter"
type="com.fritz.demo.DemoActivity.DemoPresenter" />
<variable
name="item"
type="com.fritz.demo.Bean.InfoBean" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:onTextChanged="@{presenter.onTextChanged}" />
<TextView
android:id="@+id/txtTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{item.unit!=null?item.unit:item.rental}"
android:visibility="@{item.list.size()>0?View.VISIBLE:View.GONE}" />
LinearLayout>
layout>
那么我们在另一个xml引用时,就需要传递这个include需要绑定的方法和数据:
"@layout/include_demo"
app:item="@{item}"
app:presenter="@{presenter}" />
绑定数据的bean类和xml逻辑已经写好了,现在到页面的Java代码应该怎么玩耍了。
我们页面使用DataBinding加载布局文件一般来说分Activity和Fragment这两种情况:
Activity加载布局修改为:
private ActivityDemoBinding mBinding;
protected void onCreate(Bundle savedInstanceState) {
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
}
Fragment加载布局修改为:
private FragmentHomeBinding mBinding;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mBinding = FragmentHomeBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
xml里面一切有id的view,都已经在DataBinding生成的实体类中被初始化完成了,只需要直接通过DataBinding的实例访问即可,如:
mBinding.txtTitle.setText(“Hello ,DataBinding”);
不过一般都不需要用到DataBinding来获取控件的实例,因为大多可以在xml中将控件逻辑处理好。具体的Java代码处理逻辑如下:
public class DemoActivity extends AppCompatActivity {
private ActivityDemoBinding mBinding;
private InfoBean mBean;
private List mList = new ArrayList<>();
private boolean mShow;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_demo);
initData();
}
private void initData() {
mList.add("welcome");
mBean = new InfoBean("Hello DataBinding", "","", mList);
//绑定数据
mBinding.setItem(mBean);
//绑定方法
mBinding.setPresenter(new DemoPresenter());
}
public class DemoPresenter {
public void onSetListSize(View view) {
if (!mShow) {
mList.clear();
} else {
mList.add("welcome");
}
mShow = !mShow;
mBean.setList(mList);
}
public void onTextChanged(CharSequence sequence, int start, int before, int count) {
if (!TextUtils.isEmpty(sequence.toString())) {
mBean.setUnit(null);
mBean.setRental(sequence.toString());
} else {
mBean.setUnit("Hello DataBinding");
}
}
public void onItemClick(InfoBean bean) {
Toast.makeText(DemoActivity.this, bean.getUnit(), Toast.LENGTH_SHORT).show();
}
}
}
我们在平时在xml里面总有一些控件的属性方法是没有法子去调用,但是通过DataBinding就可以比较方便去调用。
DataBinding在遇到绑定属性时会自动去查找它的get/set方法。就拿TextView来举个例子好了,我们将上面的TextView的xml代码做如下的修改:
id="@+id/txtTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:text="@{item.unit!=null?item.unit:item.rental}" />
将android:text改为上面的app:text,这时候编译运行是没有问题的。但是如果DataBinding无法找到对应的方法,那么在编译期间就会报错了。利用这个特性,对于一些自定义View,就让它没有提供我们需要的自定义属性或setXXX方法,我们可以选择直接依据它的setXXX方法的名字来绑定属性或者继承这个View,添加我们需要的setXXX方法。不过,只要添加一个setXXX方法就来再创建一个类来继承View也太浪费了,DataBinding还有更好的方法。
这个注解是用来关联控件中提供的方法的。当控件的某些属性的setXXX方法并没有对应的自定义属性,就需要 @BindingMethod 来“牵绳拉线”,创建一个新的自定义属性。关于这个,可以看下DataBinding的源码里面的ImageViewBindingAdapter这一部分的代码:
它将ImageView里面的两个方法关联了两个新的xml属性,这样子可以更加方便我们在xml中使用这两个方法。
终于说着这个强大的家伙了。当控件里面没有提供某个属性的setXXX方法,又或者这个setXXX方法名字我们不喜欢,还有就是明明就是设置控件的某个属性的,但方法名却不是以set开头的,这些情况我们可以使用BindingAdapter这个强大的注解。
关于这个BindingAdapter的使用我们是可以到SDK里面这个位置查看:
sources\android-25\android\databinding\adapters
如果没有android-25,可以到android-24或23里面,都一样的。这里有很多Google写好的demo,我们有什么不懂的地方可以直接看这些demo来照着写。这里我分享下我平时利用BindingAdapter来使用RecyclerView时的写法,如下所示:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="adapter"
type="android.support.v7.widget.RecyclerView.Adapter" />
<variable
name="layoutManager"
type="android.support.v7.widget.RecyclerView.LayoutManager" />
data>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
app:adapter="@{adapter}"
app:layoutManager="@{layoutManager}" />
layout>
直接在xml里面给RecyclerView绑定adapter和layoutManager,要做到这一点当然少不了强大的BindingAdapter:
public class UtilsBindingAdapter {
@BindingAdapter("layoutManager")
public static void setLayoutManager(RecyclerView view, RecyclerView.LayoutManager manager) {
view.setLayoutManager(manager);
}
@BindingAdapter("adapter")
public static void setAdapter(RecyclerView view, RecyclerView.Adapter adapter) {
view.setAdapter(adapter);
}
}
如果还需要给RecyclerView设置什么的话,完全可以按照上面的去写。在Java代码中的调用就是:
mBinding.setLayoutManager(new LinearLayoutManager(this));
mBinding.setAdapter(new RecyclerViewAdapter(this));
在 xml 中为属性赋值时,如果变量的类型与属性不一致,这时候我们就可以使用BindingConversion。
比方说下面代码中如果要为属性 android:background 赋值一个 int 型的 color 变量:
"@{isError.get() ? @color/red : @color/white}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_height="@{height}" />
那么只需要定义一个标记了 @BindingConversion 的静态方法即可(方法的定义位置可以随意):
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
但是使用这转换器属性时我们必须要小心,因为DataBinding是不懂得区分是否真的需要使用整个转换器的。比方说我们再创建两个:
@BindingConversion
public static int convertColorToStringOne(int color) {
switch (color) {
case Color.RED:
return R.string.red;
case Color.WHITE:
return R.string.white;
}
return R.string.app_name;
}
@BindingConversion
public static int convertColorToStringTwo(int color) {
switch (color) {
case ColorBLACK:
return R.string.black;
case Color.GREEN:
return R.string.green;
}
return R.string.app_name;
}
这是两个将一个color的整型数转为指定的颜色文本的方法. DataBinding只要找到了这个convertColorToStringOne就不懂得再往下找了。
RecyclerView是Google推出用于取代ListView和GridView的控件,它的布局,装饰和刷新机制都足以让我们放弃ListView和GridView。
上面已经提到了RecyclerView,现在具体说下DataBinding应该怎么绑定RecyclerView。
首先,我们需要定义一个基类的ViewHolder,方便我们使用DataBinding:
public class BindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
private T mBinding;
public BindingViewHolder(T binding) {
super(binding.getRoot());
mBinding = binding;
}
public T getBinding() {
return mBinding;
}
}
接着RecyclerView的Adapter直接使用这个BindingViewHolder,在onCreateViewHolder里面生成该ViewHolder:
public BindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(mInflater, R.layout.item_xxx,parent, false);
return new BindingViewHolder(binding);
}
然后在onBindViewHolder里面进行数据绑定和设置Listener:
@Override
public void onBindViewHolder(BindingViewHolder holder, int position) {
final String item = mDatas.get(position);
//给ViewHolder的xml里面的item变量进行数据绑定
holder.getBinding().setVariable(BR.item, item);
//建立绑定关系,在主线程中实时更新任何进行了绑定的数据
holder.getBinding().executePendingBindings();
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mListener != null) {
mListener.onItemClick(item);
}
}
});
}
这里创建接口并回调就不过多赘述了。核心的东西已经说完了,接着说下RecyclerView的DataBinding的第三方库吧。
目前Github上关于简化RecyclerView的DataBinding的方案我知道的有三种:
https://github.com/evant/binding-collection-adapter
https://github.com/radzio/android-data-binding-recyclerview
https://github.com/markzhai/DataBindingAdapter
前两者都可以让你少写代码,并且功能十分强大。但是我觉得灵活性是第三个比较好。这也是我目前在项目中使用的。这个项目的使用方法比较简单,可以直接去看它的ReadMe,这里就不多说了。
DataBinding 和 findViewById() 并不是互斥的,DataBinding的源码里面也是用到了findViewById()。如果某些情况真的不适合使用DataBinding,那就用回findViewById吧。
xml 文件中不要出现过于复杂业务逻辑,只出现简单的 UI 相关的表达式。不要以为Data Binding是万能的,而想尽办法把逻辑写在xml中。往往这个时候你就应该将逻辑写在绑定的ViewModel里面。
有时候因为修改接口等原因要修改绑定的bean类,这时候偶尔会遇到一些神奇的bug。不要慌张,直接clean一下项目再make project一次往往就好了
原因上面已经说了。但是它也不失为一个很好用的注解,比方说使用它来进行字体的自定义。具体可以参照下面的文章:
使用DataBinding来进行字体的自定义