Android数据绑定(DataBinding)
什么是Android数据绑定(DataBinding)?
Android数据绑定是一个Google官方发布的帮助开发者处理视图与数据交互的支持库。
数据绑定是如何工作的?
数据绑定在编译时运行,处理视图文件中发现的表达式并在应用程序中生成代码,该库包含了应用程序中的常见代码。
优点:
- 省去了findViewById()
- 兼容到Android2.1(API 7)
- 不使用反射,保证了性能
- 支持绝大部分的 Java 写法
- 最大程度减少绑定应用程序逻辑与视图所必需的代码
- 支持双向绑定,即数据改变时可更新视图,反之亦然
- 支持在任意线程更新数据(RecyclerView 和 ListView的数据除外 )
- 避免了因数据导致的空指针,当绑定的数据无效时,视图会显示绑定数据类型的默认值
Android Studio对其的支持:
- 语法高亮显示
- 标记错误语法
- XML代码补全
- 快速跳转引用
注意:数组和通用类型(如Observable类)可能会在没有错误时显示错误。
准备使用
为了更好地进行Android开发,本人强烈建议使用Android Studio并保持Android Studio 与 Gradle为最新版本
配置数据绑定使用环境:
-
Android 数据绑定需要Android Studio 1.3及更高版本
-
Gradle 1.5.0-alpha1及更高版本
-
配置相应模块(Module)的build.gradle(若其他模块要用到数据绑定也需要此配置)
android { .... dataBinding { enabled = true } }
在视图文件中绑定数据
-
首先准备准备一个数据类(请注意,由于视图要访问该对象的私有变量,所以必须提供getter)
public class Person{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } }
-
在相应的视图文件中引入这个数据类(根节点必须为layout)
- 使用variable标签方式引入数据类,name为变量名,type为数据类,必须要有完整的包名(Android Studio 支持输入类名自动查找补全)
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="person" type="包名.Person" /> </data> </layout>
- 使用import方式引入数据类,用这种方式引入数据类还可以使用它的静态变量和方法
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="包名.Person" /> <variable name="person" type="Person" /> </data> </layout>
若出现不同包的同名类则可以在import 时 使用 alias 来指定一个别名,例如:
<import type="包名.Person" alias="OtherPerson"/> <variable name="otherPerson" type="OtherPerson"/>
-
在视图中绑定数据,请注意数据类型匹配,可在后用defalut设置默认值,默认值会显示在预览视图中。
<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{person.name}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(person.age})}"/> </LinearLayout>
还可以绑定任何位置的点击事件,使用
::
来绑定点击事件public class ClickEvents { public void onBtnClick(View view) { ... } }
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="clickEvents" type="包名.ClickEvents"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BTN" android:onClick="@{clickEvents::onBtnClick}"/> </LinearLayout> </layout>
绑定Array、List、Map、Sparse的数据(不支持Set)
String[] array = {"测试数组"}; List<String> list = new ArrayList<>(); list.add("测试集合"); Map<String, String> map = new HashMap<>(); map.put("测试1", "测试Map"); SparseArray<String> sparseArray = new SparseArray<>(); sparseArray.append(0, "测试SparseArray"); binding.setArray(array); binding.setList(list); binding.setMap(map); binding.setSparseArray(sparseArray);
注意:
- 在xml中设置范型时要将"<>"换成相应的实体,"<" 对应
<
, ">" 对应>
- 除Array外,其他的还可以用get()来取值
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="java.util.Map" /> <import type="java.util.List" /> <import type="java.lang.String" /> <import type="android.util.SparseArray"/> <!--以下爆红正常,运行无错。--> <variable name="array" type="String[]" /> <variable name="list" type="List<String>" /> <variablename="map" type="Map<String, String>" /> <variablename="sparseArray" type="SparseArray<String>" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/tv_array" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{array[0]}" android:textSize="18sp" /> <TextView android:id="@+id/tv_list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="@{list[0]}" android:textSize="18sp" /> <TextView android:id="@+id/tv_map" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="@{map[`测试1`]}" android:textSize="18sp" /> <TextView android:id="@+id/tv_sparseArray" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="@{sparseArray[0]}" android:textSize="18sp" /> </LinearLayout> </layout>
- 在xml中设置范型时要将"<>"换成相应的实体,"<" 对应
-
在程序中绑定视图并设置数据
注意:
-
绑定数据的视图会自动根据其视图名字去掉"_"并在最后加上Binding生成驼峰式类名的绑定文件,例:activity_main => ActivityMainBinding
-
视图中设置了id的元素会在对应的绑定类中生成一个对象,其命名方式同上,首字母小写,例:tv_name => tvName
-
绑定视图
-
在Activity中
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
-
在Fragment中
FragmentMainBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false); // or FragmentMainBinding binding = FragmentMainBinding.inflate(inflater, container, false);
-
在RecyclerView或者ListView中
ItemBinding binding = ItemBinding.inflate(layoutInflater, viewGroup, false); //or ItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
-
-
设置数据
Person person = new Person("GavinRowe", 21); binding.setPerson(person);
运行程序就能看到数据了!
-
改变数据并通知视图
-
继承BaseObservable方式,使用数据类继承BaseObservable
public class Person extends BaseObservable{ private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Bindable public String getName() { return name; } @Bindable public int getAge() { return age; } public void setName(String name) { this.name = name; notifyPropertyChanged(BR.name); } public void setAge(int age) { this.age = age; notifyPropertyChanged(BR.age); } }
标注变量写法
private @Bindable String name;
继承BaseObservable后将会获得两个公共的通知视图更新的方法:1. 通知所有数据更新 notifyChange(); 2. 通知特定数据更新 notifyPropertyChanged(int fieldId),参数为BR.java文件中对应的变量标志。
被@Bindable注解标注的getter或者变量将会在一个位于包名下的BR.java文件中生成一个对应的变量标志,例:上面已经被标注的getName()与getAge()对应BR.name与BR.age。
注:@Bindable注解不是必须的,不使用时就必须调用notfyChange()或notifyPropertyChanged(BR._all)来通知视图更新所有数据
-
使用ObservableField方式
public class Person{ public ObservableField<String> name = new ObservableField<>() ; public ObservableInt age = new ObservableInt(); public Person(String name, int age) { this.name.set(name); this.age.set(age); } }
对应的ObservableField将会提供getter和setter,通过setter设置数据后会自动通知视图更新数据
双向绑定
双向绑定,即数据改变时可通知视图改变,视图改变时同时改变数据
用法:在绑定数据时将@{data} 变为 @={data}
-
数据类
public class Person{ public ObservableField<String> name = new ObservableField<>(); public Person(String name) { this.name.set(name); } }
-
视图绑定数据
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <import type="包名.Person" /> <variable name="person" type="Person" /> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{person.name}"/> <EditText android:gravity="center" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" android:text="@={person.name}"/> </LinearLayout> </layout>
-
绑定视图并设置数据
ActivityMainBinding binding = DataBindingUtil.setContentView(this,R.layout.activity_main); binding.setPerson(new Person("GavinRowe"));
绑定数据在RecyclerView中的应用
通过一个多布局RecyclerView来演示数据绑定
-
数据类
public class Person { public ObservableField<String> name = new ObservableField<>(); public ObservableInt age = new ObservableInt(); public Person(String name, int age) { this.name.set(name); this.age.set(age); } }
-
页面视图,由于要为RecyclerView设置适配器以及LayoutManager所以需要为它设置一个ID便于查找
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="person" type="包名.Person" /> </data> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="onAddDataClick" android:text="添加数据" /> <android.support.v7.widget.RecyclerView android:id="@+id/rv_people" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout> </layout>
-
item_rv_people_01视图
<?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="person" type="包名.Person" /> </data> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_rv_people" android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:paddingEnd="12dp" android:paddingStart="12dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{person.name}" android:textSize="16sp" /> <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:text="@{String.valueOf(person.age)}" android:textSize="16sp" /> </LinearLayout> </layout>
-
item_rv_people_02视图
<?xml version="1.0" encoding="utf-8"?> <layout> <data> <variable name="person" type="包名.Person" /> </data> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/item_rv_people" android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center" android:paddingEnd="12dp" android:paddingStart="12dp"> <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{String.valueOf(person.age)}" android:textSize="16sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:text="@{person.name}" android:textSize="16sp" /> </LinearLayout> </layout>
-
MultiLayoutPeopleAdapter多布局适配器
注意:
- ViewHolder的写法,通过ViewDataBinding父类来接收两个不同视图的绑定类,两个布局共享的数据就是person,由于ViewDataBinding没有setPerson(),所以通过setVariable(BR.person, person) 方法设置键值对的方式来将Person对象绑定到两个不同的视图
- getItemViewType(int position)直接返回对应Item视图的ID,通过DataBindingUtil就可以绑定任何想绑定的视图了
- 关于executePendingBindings(),当你的数据改变时,数据绑定在一个动画帧之前刷新,executePendingBindings()可以立即强制刷新,此操作必须在UI线程进行
- 若要分别对视图操作,则可将绑定类引用向下转型,然后分别获取视图来设置进行操作
public class MultiLayoutPeopleAdapter extends RecyclerView.Adapter<MultiLayoutPeopleAdapter.PeopleViewHolder> { private List<Person> people; private static Activity mActivity; MultiLayoutPeopleAdapter(Activity activity, List<Person> people) { mActivity = activity; this.people = people; } @Override public PeopleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return PeopleViewHolder.create(LayoutInflater.from(parent.getContext()), parent, viewType); } @Override public void onBindViewHolder(PeopleViewHolder holder, final int position) { holder.bindTo(people.get(position)); // 判断布局 if (holder.mBinding instanceof ItemRvPeople01Binding) { ItemRvPeople01Binding item01 = (ItemRvPeople01Binding) holder.mBinding; item01.itemRvPeople.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mActivity, "item01的" + position + "被点了!", Toast.LENGTH_SHORT).show(); } }); } else { ItemRvPeople02Binding item02 = (ItemRvPeople02Binding) holder.mBinding; item02.itemRvPeople.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mActivity, "item02的" + position + "被点了!", Toast.LENGTH_SHORT).show(); } }); } } @Override public int getItemCount() { return people.size(); } @Override public int getItemViewType(int position) { if (position % 2 == 0) { return R.layout.item_rv_people_01; } else { return R.layout.item_rv_people_02; } } static class PeopleViewHolder extends RecyclerView.ViewHolder { ViewDataBinding mBinding; static PeopleViewHolder create(LayoutInflater inflater, ViewGroup parent, int type) { ViewDataBinding binding = DataBindingUtil.inflate(inflater, type, parent, false); return new PeopleViewHolder(binding); } private PeopleViewHolder(ViewDataBinding binding) { super(binding.getRoot()); mBinding = binding; } void bindTo(Person person) { mBinding.setVariable(BR.person, person); mBinding.executePendingBindings(); } } }
-
绑定视图
private List<Person> people; private MultiLayoutPeopleAdapter multiLayoutPeopleAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); people = new ArrayList<>(); multiLayoutPeopleAdapter = new MultiLayoutPeopleAdapter(this, people); binding.rvPeople.setLayoutManager(new LinearLayoutManager(this)); binding.rvPeople.setAdapter(multiLayoutPeopleAdapter); } public void onAddDataClick(View view) { people.add(new Person("国哥", 21)); people.add(new Person("哥哥", 27)); people.add(new Person("姐姐", 30)); people.add(new Person("小红", 16)); people.add(new Person("小蓝", 15)); people.add(new Person("小橙", 14)); people.add(new Person("小绿", 13)); people.add(new Person("小黄", 12)); people.add(new Person("小花", 6)); people.add(new Person("小德", 5)); people.add(new Person("小梦", 4)); multiLayoutPeopleAdapter.notifyDataSetChanged(); }
注解:@Bindable
此注解可用来标注变量和getter,被标注后将会在一个位于包名下的BR.java文件中生成一个对应的变量标志。
注解:@BindingAdapter
此注解可用来标注方法,当xml中使用到该属性时就会调用其标注的方法。
单参用法:@BindingAdapter("xml属性")
,参数可以为已有的xml属性,比如android:src
,也可以自定义属性直接在xml中使用,若自定义属性不带命名空间(如:android:
, app:
, xxx:
等 )将默认为app:
,在使用的时候请注意声明命名空间,如:xmlns:app="http://schemas.android.com/apk/res-auto"
。
多参用法:@BindingAdapter(value = {"imgUrl", "android:clickable"}, requireAll = false)
,requireAll表示是否为每个声明的属性添加绑定值,默认为true。
注意:
- 标注的方法第一个参数必须为对应的视图对象
- 标注的方法参数顺序必须与标注的xml属性顺序一致
- 在xml使用标注的属性时,其值必须用数据绑定的形式
- 标注的方法为实例方法时,该类必须先实现DataBindingComponent,然后在相应绑定类解析视图之前调用DataBindingUtil.setDefaultComponent
例:
public class Concat {
public static String content = "测试Binding";
@BindingAdapter("android:text")
public static void add(final TextView tv, String content) {
Log.d("Concat", content);
tv.setText(content.concat("Adapter"));
}
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="concat" type="包名.Concat" />
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{concat.content}"
android:textSize="18sp" />
</layout>
@BindingAdapter注解
XML中绑定数据支持的表达式
- 数学 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二进制 & | ^
- 一元运算 + - ! ~
- 三元运算 ?:
- 判断是否为空 ??(例:
android:text="@{user.name ?? user.defaultName}"
,相当于android:text="@{user.name !=null ? user.name : user.defaultName}"
) - 位运算 >> >>> <<
- 比较 == > < >= <=
- instanceof
- 方法调用
- 变量引用
- 获取数组、集合、Map的值 []
不支持:this, super, new
建议在视图中用与视图相关的简单明了的表达式,否则建议使用方法或者@BindingAdapter
错误信息
在这里我会提供一些使用数据绑定时曾遇到过的错误和可能的原因,仅供参考!
更多的如果我遇到了会更新出来,如果正在阅读此文的你遇到过一些我没有提到的问题欢迎联系我
-
android.content.res.Resources$NotFoundException
数据类型错误导致,例:给android:text
绑定数据时,该数据类型为int -
Error:(6, 27) 错误: 找不到符号 符号: 类 DataBindingComponent 位置: 程序包 android.databinding 遇到此类错误请往下拉,一般在后面会有具体错误原因
Error:(55, 29) Could not find accessor… 绑定数据时xml参数名写错了或者访问的私有变量未提供getter
Error:(148, 30) Identifiers must have user defined types from the XML file 某个数据未在xml的data标签进行导入,如果已在data标签中用variable标签导入,检查绑定位置是否用类名来引用其数据,若是,换成import标签导入数据