在传统的Android应用开发中,布局文件通常只负责应用界面的布局工作,如果需要实现页面交互就需要调用setContentView()将Activity、fragment和XML布局文件关联起来。然后通过控件的id找到控件,接着在页面中通过代码对控件进行逻辑处理。在这种传统的开发方式中,页面承担了大部分的工作量,大量的逻辑处理需要在Activity、Fragment中进行处理,因此页面显得臃肿不堪,维护起来也很困难,为了减轻页面的工作量,Google提出了DataBiiding(视图绑定)。
DataBinding的出现让布局文件承担了原本属于Activity、Fragment页面的部分逻辑,使得Activity页面和XML布局之间的耦合度进一步降低。DataBinding主要有以下特点:
事实上,DataBinding和MVVM架构是分不开的,DataBinding正是Google为了能够更好的实现MVVM架构而实现的。
视图绑定功能可按模块启用,要在某个模块中启用视图绑定,请将 viewBinding 元素添加到build.gradle 文件中,如下所示。
android {
...
viewBinding {
enabled = true
}
}
DataBinding 是一个 support 包,添加完后上面的脚本后会发现工程的的External Libraries中多了四个aar包。分别是adapters、commen、runtime和viewbinding。
使用DataBinding时,如果希望在生成绑定类时忽略某个布局文件,可以将 tools:viewBindingIgnore="true"
属性添加到相应布局文件的根视图中,如下所示。
...
使用DataBinding的第一步,就是先改造XML文件。其实改造布局文件也特别简单,只需要在原来文件内容的基础上,最外层改为
标签即可,如下所示。
...
...
当然,我们也可以选中根布局,按住【Alt + 回车键】然后选择 【Convert to data binding layout】也可以生成 DataBinding 需要的布局规则。在布局最外层加layout标签后,重新编译项目,DataBinding库就会生成对应的Binding类,该类用来实现XML布局文件与Model类的绑定,代码如下。
public class ActivityMainBindingImpl extends ActivityMainBinding {
@Nullable
private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = null;
}
...
// 此处省略
...
}
生成的ActivityMainBindingImpl代码位于app/build目录下。生成Binding类的名字很特殊,它与XML布局文件的名字有对应关系,具体的联系就是,以XML布局文件为准,去掉下划线,所有单次以大驼峰的形式按顺序拼接,最后再加上Binding。比如,我的XML布局文件名是activity_main.xml,生成的Binding类名就是ActivityMainBinding。
没有使用DataBinding的时候,为了将XML布局文件与Activity进行绑定,需要调用Activity的setContentView()方法,或者是在Fragment中调用LayoutInflate的inflate()方法来进行布局的绑定。如果使用了DataBinding之后,就需要使用DataBindingUtil类来进行视图的绑定,如下所示。
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding= DataBindingUtil.setContentView(this,R.layout.activity_main);
}
}
使用DataBindingUtil类的setContentView()方法对Activity进行绑定,其返回值就是工具生成的Binding类。如果是Fragment,则对应的绑定布局的代码如下。
private ActivityMainBinding binding;
@Override
public View onCreateView (LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
binding = ActivityMainBinding.inflate(inflater, container, false);
View view = binding.getRoot();
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
经过前面的步骤后,我们已经使用DataBinding将XML文件与UI组件绑定起来,如果要在XML文件中接受Model数据,就需要用到data标签与variable标签。
在XML文件的layout标签下,创建data标签,在data标签中再创建variable标签,variable标签主要用到的就是name属性和type属性,类似于Java语言声明变量时,需要为该变量指定类型和名称。新建一个名为UserModel的实体类,代码如下。
public class UserModel {
private String firstName;
private String lastName;
public UserModel(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
然后在布局的 data 标签里声明要使用到的变量名、类的全路径等信息,如下所示。
... //省略其他代码
如果 User 有多处用到,也可以直接将之 import 进来,这样就不用每次都指明整个包名路径了,而 java.lang.* 包中的类会被自动导入,所以可以直接使用。
如果存在 import 的类名相同的情况,可以使用 alias 指定别名,如下所示。
在XML文件中声明好variable属性后,接下来就可以在XML使用它了。使用variable属性时需要使用到布局表达式: @{ }
。可以在布局表达式@{ }中获取传入variable对象的值,如下所示。
然后,我们在UI界面中使用Binding类为每一个variable标签使用set方法传递数据,如下所示。
binding= DataBindingUtil.setContentView(this,R.layout.activity_main);
UserModel user =new UserModel("zhang", "beijing");
binding.setUser(user);
DataBinding的本身是对View层状态的一种观察者模式的实现,通过让View与ViewModel层可观察的对象(比如LiveData)进行绑定,当ViewModel层数据发生变化,View层也会自动进行UI的更新,此种场景称之为单向绑定。
但实际开发中,单向绑定并不能足够所有的需求。例如有下面的场景:如果布局中有一个EditText,当用户在输入框中输入内容时,我们希望对应的Model类能够实时更新,这就需要双向绑定,DataBinding同样支持这样的能力。
实现双向绑定需要用到ObservableField类,它能够将普通的数据对象包装成一个可观察的数据对象,数据可以是基本类型变量、集合,也可以是自定义类型。为了实现双向绑定,我们需要先定义一个继承自BaseObservable的ViewModel类,并提供get和set方法,如下所示
public class UserViewModel extends BaseObservable {
private String name;
private String address;
private int age;
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
... //省略其他代码
}
然后,我们在XML布局文件中使用DataBinding,如下所示。
需要注意的是,不同于单向绑定,之前的布局表达式是@{}
表达式,双向绑定使用的布局表达式是@={}
,多了一个等号。
DataBinding 可以绑定普通数据对象(非 Observable/LiveData),上述的都是绑定普通数据,
绑定可观察数据意味着当数据变化时 UI 会跟着一起变化,绑定可观察数据有三种方式:fields、 collections和 objects.
public class MainActivity extends BaseActivity {
private static final String TAG = MainActivity.class.getName();
ObservableField inputs = new ObservableField<>("演示ObservableField");
@Override
protected void initView() {
dataBinding.setInput(inputs);
}
}
fields变量绑定主要是引用带有泛型参数的ObservableField来创建,其中泛型是数据类型。
对于基本类型和 Parcelable 我们可以直接使用对应的包装类:
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
collections数据绑定(集合数据)
public class MainActivity extends BaseActivity {
private static final String TAG = MainActivity.class.getName();
ObservableMap maps = new ObservableArrayMap<>();
ObservableList lists = new ObservableArrayList<>();
int listSign = 0;
boolean isName = true;
Handler handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
if (isName){
isName = false;
dataBinding.setMapKey("name");
}else{
isName = true;
dataBinding.setMapKey("age");
}
handler.removeMessages(1);
handler.sendEmptyMessageDelayed(1, 2000);
break;
case 2:
if (listSign<5){
dataBinding.setListSubscript(listSign);
listSign++;
}else{
listSign = 0;
dataBinding.setListSubscript(listSign);
}
handler.removeMessages(2);
handler.sendEmptyMessageDelayed(2, 2000);
break;
}
}
};
@Override
protected void data() {
maps.clear();
maps.put("name","Map集合演示:1");
maps.put("age","Map集合演示:2");
dataBinding.setMap(maps);
lists.clear();
for (int i = 0; i < 5; i++) {
lists.add("list集合数据演示:" + i);
}
dataBinding.setList(lists);
handler.removeMessages(1);
handler.sendEmptyMessageDelayed(1, 2000);
handler.removeMessages(2);
handler.sendEmptyMessageDelayed(2, 2000);
}
}
对于集合而言,List是通过下标去取值的,而Map是通过key取value,我通过定义两个变量listSubscript和mapKey,通过代码修改这两个变量来改变取值。
对于集合提供的包装类
ObservableArrayList
ObservableArrayMap
public class UserBean extends BaseObservable {
private String name;
private int age;
public UserBean(String name, int age) {
this.name = name;
this.age = age;
notifyPropertyChanged(BR.user);
}
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyPropertyChanged(BR.name);
}
@Bindable
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}
Handler handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 3:
age++;
UserBean userBean = new UserBean("tao",age);
dataBinding.setUser(userBean);
handler.removeMessages(3);
handler.sendEmptyMessageDelayed(3, 2000);
break;
}
}
};
@Override
protected void data() {
handler.removeMessages(3);
handler.sendEmptyMessageDelayed(3, 2000);
}
对应对象而言必须BaseObservable或者Observable,BaseObservable相对于Observable比较完善,使用更简单,get方法需要加注解@Bindable,set方法使用notifyPropertyChanged通知对于变量更新。
Observable 接口具有添加和移除监听器的机制,但何时发送通知必须由自己决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable 类。实现 BaseObservable 的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable 注释,然后在 setter 中调用 notifyPropertyChanged() 方法
前面我们介绍了DataBinding的一些基本用法,我们可以在布局文件中对控件某些属性进行赋值,使得Model类数据直接绑定在布局中,而且Model属性发生变化时,布局文件中的内容可以即时刷新。除了这些简单的使用场景外,我们还可以使用DataBinding响应用户事件。
我们对布局文件做一下修改,在里面添加一个控件,然后在Activity中添加如下代码。
binding.btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
除此之外,我们还可以使用另外一种方式。布局表达式不仅可以传入对象的属性,也可以调用对象的方法。首先创建一个工具类,在类中定义响应事件的方法,如下所示。
public class ButtonClickListener {
public void onClick(View view) {
Log.d("ButtonClickListener","onClick...");
}
}
然后在布局文件中添加点击事件的代码,如下所示。
...//省略其他代码
在上面的代码中,首先在data标签中为ButtonClickListener类声明对象,在Button的onClick属性中传入布局表达式即可。
在Android应用开发中,为了能够让布局文件得到复用,在编写布局的时候我们经常会使用include标签,相同结构与内容的布局文件就可以在多处使用。但是如果一个布局文件中使用了DataBinding,同时也使用了include标签,那么如何使用nclude标签引入的布局文件中中的数据呢。
此时,我们需要在同一级页面的include标签中,通过命名控件xmlns:app来引入布局变量User,将数据对象传递给二级页面,如下所示。
... //省略其他代码
布局表达式中直接传入页面变量user,include标签属性值可以任意取名,但是要注意的是,在二级页面的variable标签中的name属性,必须与一级页面中的include标签属性名一致,如layout_data_binding的代码所示。
使用DataBinding库时,DataBinding会针对控件属性生成对应的XXXBindingAdapter类,如TextViewBindingAdapter类,其对TextView的每个可以使用DataBinding的属性都生成了对应的方法,而且每个方法都使用了@BindingAdapter注解,注解中的参数就是对应View的属性。
@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings({"WeakerAccess", "unused"})
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
@BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
@BindingMethod(type = TextView.class, attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
@BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = "setAllCaps"),
@BindingMethod(type = TextView.class, attribute = "android:textColorHighlight", method = "setHighlightColor"),
@BindingMethod(type = TextView.class, attribute = "android:textColorHint", method = "setHintTextColor"),
@BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
@BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
public class TextViewBindingAdapter {
private static final String TAG = "TextViewBindingAdapters";
@SuppressWarnings("unused")
public static final int INTEGER = 0x01;
public static final int SIGNED = 0x03;
public static final int DECIMAL = 0x05;
@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.
}
view.setText(text);
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
@BindingAdapter({"android:autoText"})
public static void setAutoText(TextView view, boolean autoText) {
KeyListener listener = view.getKeyListener();
TextKeyListener.Capitalize capitalize = TextKeyListener.Capitalize.NONE;
int inputType = listener != null ? listener.getInputType() : 0;
if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
capitalize = TextKeyListener.Capitalize.CHARACTERS;
} else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
capitalize = TextKeyListener.Capitalize.WORDS;
} else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
capitalize = TextKeyListener.Capitalize.SENTENCES;
}
view.setKeyListener(TextKeyListener.getInstance(autoText, capitalize));
}
}
BindingAdapter类中,所有的方法都是static方法,并且每个方法都使用了@BindingAdapter注解,注解中声明所操作的View属性,当使用了DataBinding的布局文件被渲染时,属性所对应的static方法就会自动调用。
除了使用库自动生成的BindingAdapter类之外,开发者也可以自定义BindingAdapter类,供开发者来实现系统没有提供的属性绑定,或者是修改原有的属性。
例如,有下面这样一个需求:我们希望可以根据图片地址动态的改变显示图片。如果使用BindingAdapter 如何实现呢?
我们编写一个处理图片的自定义BindingAdapter类。然后定义一个静态方法,主要用于添加 BindingAdapter 注解,注解值是 ImageView 控件自定义的属性名,如下所示。
public class ImageBindingAdapter {
@BindingAdapter({"url"})
public static void loadImage(ImageView view, String url) {
if(!TextUtils.isEmpty(url)){
Glide.with(view)
.load(url)
.centerCrop()
.placeholder(R.drawable.ic_launcher_background)//加载中显示的图片
.error(R.drawable.ic_launcher_foreground)// 错误后显示的图片
.into(view);
}
}
}
可以发现没,loadImage()静态方法的两个参数,第一个参数必须是所操作的View类型,第二个参数是图片的地址。当 ImageView 控件的 url 属性值发生变化时,dataBinding 就会将 ImageView 实例以及新的 url 值传递给 loadImage() 方法,从而实现动态改变 ImageView的相关属性。
然后,我们在 XML 文件中关联变量值,如下所示。
需要说明的是,请注意布局文件的最外层包含以下命名控件,这样才能调用@BindingAdapter
标签定义的静态方法。
xmlns:app="http://schemas.android.com/apk/res-auto"
需要说明的是,Activity和XML绑定的代码一定要用DataBindingUtil,如下所示。
DataBindingUtil.setContentView(this,R.layout.activity_main);
经过上面的处理后,我们就可以很方便的使用 imageUrl 属性来加载网络图片了,并且不用担心线程切换问题,DataBinding 库会自动完成线程切换。运行上面的代码,效果如下所示。
有时候,我们需要自定义多个属性,那如何处理呢?和一个参数一样,我们只需要使用BindingAdapter添加参数即可,如下所示。
public class ImageBindingAdapter {
@BindingAdapter(value = {"url", "placeholder", "error"})
public static void loadImage(ImageView view, String url, Drawable placeholder, Drawable error) {
if(!TextUtils.isEmpty(url)){
RequestOptions options = new RequestOptions();
options.placeholder(placeholder);
options.error(error);
Glide.with(view.getContext())
.load(url)
.apply(options)
.into(view);
}
}
}
然后在布局中传入属性值即可,如下所示。
在某些情况下,我们需要对设置属性时类型进行转化,类型转化可以借助注解 @BindingConversion
来完成。例如,android:background
属性接收的是一个 Drawable ,但是我们在使用的时候需要给databinding 表达式中设置一个颜色值,此时就需要 @BindingConversion
。
首先,创建一个颜色转化的类ColorConversion,用于将颜色值转化为Drawable,如下所示。
public class ColorConversion {
@BindingConversion
public static ColorDrawable colorToDrawable(int color){
return new ColorDrawable(color);
}
}
然后,创建一个布局文件,添加如下代码。
列表布局在Android应用开发中是非常常见的场景,实现列表布局需要使用RecyclerView控件,DataBinding支持在RecyclerViieew中实现数据绑定。
使用RcyclerView,就需要用到Adapter,在Adapter中实例化Item布局,然后将List中的数据绑定到布局中,而DataBinding就可以帮助开发者实例化布局并绑定数据。
首先,我们编写Adapter的item布局,在item布局中使用DataBinding将User数据进行绑定,item_user.xml的代码如下所示。
接下来,编写Adapter类业务处理,UserAdapter的代码如下所示。
public class UserAdapter extends RecyclerView.Adapter {
private List mDataList;
public UserAdapter(List mDataList) {
this.mDataList = mDataList;
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemUserBinding binding;
public ViewHolder(@NonNull ViewDataBinding binding) {
super(binding.getRoot());
this.binding=(ItemUserBinding)binding;
}
public ItemUserBinding getBinding() {
return binding;
}
}
@NonNull
@Override
public UserAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemUserBinding binding= DataBindingUtil.inflate((LayoutInflater.from(parent.getContext())), R.layout.item_user,parent,false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull UserAdapter.ViewHolder holder, int position) {
UserModel model=mDataList.get(position);
holder.getBinding().setUser(model);
}
@Override
public int getItemCount() {
if (mDataList.size()>0){
return mDataList.size();
}
return 0;
}
}
可以发现,之前我们都是需要在ViewHolder中进行findViewById对子控件进行实例化,由于我们使用了DataBinding,所以不再需要这些操作,只需要传入生成的Binding类即可,然后在super中调用getRoot()方法返回根View。
在RecyclerView中使用DataBinding就是如此简单,当List中的item数据发生变化时,列表中的内容也会随之更新。
然后,按照RecyclerView的基本使用方法,我们在MainActivity添加一些测试数据,并将它和UserAdapter进行绑定,代码如下。
public class MainActivity extends AppCompatActivity {
private final String TAG = "MainActivity";
private ActivityMainBinding activityMainBinding;
private List userModels;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
initData();
initRecyclerView();
}
private void initData() {
userModels = new ArrayList();
for (int i = 0; i < 10; i++) {
UserModel userModel = new UserModel("zhangsan"+1, "beijing"+i, "age"+i);
userModels.add(userModel);
}
}
private void initRecyclerView() {
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
activityMainBinding.recycle.setLayoutManager(layoutManager);
activityMainBinding.recycle.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
UserAdapter adapter = new UserAdapter(userModels);
activityMainBinding.recycle.setAdapter(adapter);
}
}
executePendingBindings
executePendingBindings作用是执行所有待处理的绑定操作。在Android数据绑定(Data Binding)框架中,当数据发生变化时,这些变化不会立即反映在UI上,而是被暂时存储起来,等待合适的时间执行。通过调用executePendingBindings()
方法,可以强制执行所有待处理的绑定操作,确保数据的变化能够及时反映在用户界面上。这样做的好处是,它提供了一种机制来确保数据与UI之间的同步,避免了因为等待系统自动处理而可能导致的延迟或不同步的问题。
在Android开发中,数据绑定是一个重要的概念,它允许开发者将数据模型直接绑定到用户界面上,从而减少了大量繁琐的代码编写。通过使用数据绑定库,开发者可以更加专注于业务逻辑和数据模型的设计,而无需过多关心UI的更新和渲染。executePendingBindings()
方法正是这一机制的一部分,它确保了当数据发生变化时,这些变化能够迅速且准确地反映在用户界面上,提供了更加流畅和响应式的用户体验
public class LicenseAdapter extends BaseQuickAdapter< LicenseBean, BaseDataBindingHolder> {
public LicenseAdapter(@Nullable List data) {
super(R.layout.item_license, data);
}
@Override
protected void convert(@NotNull BaseDataBindingHolder bindingHolder, LicenseBean dataBean) {
ItemLicenseBinding binding = bindingHolder.getDataBinding();
if (binding != null) {
binding.setLicenseBean(dataBean);
binding.executePendingBindings();
}
}
}
在上面的代码中,对RecyclerView设置LayoutManager和Adapter属于对View的一些复杂操作,这些操作可以通过自定义BindingAdapter的方式进行简化。首先,定义一个新的属性,将数据List直接通过DataBinding在布局文件中绑定,并且将这些操作都封装到BindindAdapter中,Activity中不再需要设置LayoutManager和Adapter操作。
首先,定义BindingAdapter,如下所示。
public class UserBindingAdapter {
@BindingAdapter("users")
void setUsers(RecyclerView recyclerView, List users ) {
LinearLayoutManager layoutManager = new LinearLayoutManager(recyclerView.getContext());
layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
UserAdapter adapter = new UserAdapter(users);
recyclerView.setAdapter(adapter);
}
}
在上面的代码中,我们声明了一个新的属性users,然后使用@BindingAdapter修饰静态方法,然后在方法里面对RecyclerView设置LayoutManager和Adapter。接下来,我们只需要布局中使用DataBinding即可。