一.DataBinding的意义和优势
我们知道,布局文件通常只负责UI控件的布局工作。页面通过setContentView()方法关联布局文件,再通过UI控件的id找到控件,接着在页面中通过代码对控件进行操作。可以说,页面承担了绝大部分的工作量,为了减轻页面的工作量,Google提出了DataBinding。DataBinding的出现让布局文件承担了部分原本属于页面的工作,也使页面和布局文件之间的耦合度进一步降低。DataBinding具有以下优势:
*项目更简洁,可读性更高。部分和UI控件相关的代码可以直接在布局文件中完成
*不再需要findViewById()方法了
*布局文件可以包含简单的业务逻辑,UI控件能够直接与数据模型中的字段绑定,甚至能响应用户的交互
二.DataBinding的简单绑定
假设有这样一个需求,在Activity中通过3个TextView控件,分别展示Book对象的三个字段,书名,作者和评分。下面采用DataBinding来实现:
1.在app/build.gradle中启用数据绑定:
代码语言:javascript复制android {
dataBinding{
enabled=true
}
}
2.修改布局文件:
在布局文件的外层加入<layout>标签,并将命名空间移动到<layout>标签中,然后rebuild该项目,DataBinding会自动生成绑定该布局文件所需要的类,代码如下:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<TextView
android:id="@ id/tv_bookname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}"/>
<TextView
android:id="@ id/tv_bookauthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.author}"/>
<TextView
android:id="@ id/tv_bookrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{BookRating.ratingString(book.rate)}"/>
</LinearLayout>
</layout>
3.实例化布局文件
在没有DataBinding时,通常需要通过Activity.setContentView()方法实例化布局文件,接着通过findViewById()方法找到布局文件中对应的UI控件。有了DataBinding组件后,就可以告别findViewById()方法了,我们可以通过DataBindingUtil.setContentView()方法来实例化布局文件,该方法返回实例化后的布局文件对象,名字是布局文件的名字首字母大写然后在最后加上Binding。代码如下:
代码语言:javascript复制public class MainActivity extends AppCompatActivity {
private ActivityMainBinding activityMainBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.setBook(new Book("西游记","罗贯中",5));
}
}
4.将数据传递到布局文件
首先,在布局文件中定义布局变量<variable>,然后指定对象的类型和名字,名字可以自定义,需要注意的是,布局变量需要定义在<data>标签中,代码如下:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
代码语言:javascript复制public class Book {
private String name;
private String author;
private Integer rate;
public Book(String name, String author, Integer rate) {
this.name = name;
this.author = author;
this.rate = rate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Integer getRate() {
return rate;
}
public void setRate(Integer rate) {
this.rate = rate;
}
}
DataBinding为了方便我们使用,给布局变量提供了Setter方法,我们可以使用setBook()方法将Book对象传递给布局文件中对应的布局变量。
<data>标签用于存放布局文件中各个UI控件所需要的所有数据,这些数据类型可以是自定义类型,也可以是基本类型。
5.绑定UI控件和布局变量
android:text="@{book.name}"
6.在布局文件中引入静态类
有时,我们需要在布局文件中引入一些java工具类,帮助我们处理一些简单的逻辑,例如将阿拉伯数字转化为对应的中文分数。我们可以在布局文件中通过<import>标签导入静态工具类。
代码语言:javascript复制public class BookRating {
public static String ratingString(int rate){
switch(rate){
case 0:
return "零星";
case 1:
return "一星";
case 2:
return "二星";
case 3:
return "三星";
case 4:
return "四星";
case 5:
return "五星";
}
return "error";
}
}
三.DataBinding响应事件
我们通过Button控件来演示DataBinding如何响应onClick事件。
1.编写一个名为EventHandleListener的类,用于接收和响应Button的onClick事件。代码如下:
代码语言:javascript复制public class EventHandleListener {
private Context context;
public EventHandleListener(Context context){
this.context=context;
}
public void onClick(View view){
Toast.makeText(context, "Button1 Clicked.", Toast.LENGTH_SHORT).show();
}
}
2.在布局文件中的<data>标签中定义布局变量:
代码语言:javascript复制 <variable
name="EventHandleListener"
type="com.example.databinding.EventHandleListener"/>
3.实例化EventHandleListener类,并传入布局文件:
代码语言:javascript复制 activityMainBinding.setEventHandleListener(new EventHandleListener(this));
4.通过布局表达式,调用EventHandleListener中的方法:
代码语言:javascript复制 android:onClick="@{EventHandleListener::onClick}"
那如果想要将布局变量book也传入onClick()方法,该怎么做呢?答案是使用lambda表达式,代码如下:
代码语言:javascript复制 <Button
android:id="@ id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="按钮2"
android:onClick="@{()->EventHandleListener.onClick(book)}"/>
代码语言:javascript复制 public void onClick(Book book){
Toast.makeText(context,book.toString(),Toast.LENGTH_SHORT).show();
}
实现按钮的点击事件还有一种方式,因为比较简单,下面直接看代码:
代码语言:javascript复制@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.button3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "button3 clicked.", Toast.LENGTH_SHORT).show();
}
});
}
四.二级页面的绑定
在这里,我们将Activity/Fragment直接对应的页面定义为一级页面,将在一级页面通过<include>标签进行引用的页面定义为二级页面。在一级页面中设置好布局变量book后,便可以直接接收来自页面的数据了,然后和UI控件进行绑定;不仅如此,布局变量book同时也是命名空间xmlns:app的一个属性。一级页面正是通过命名空间xmlns:app引用布局变量book,将数据对象传递给二级页面的,代码如下:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8" ?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="book"
type="com.example.databinding.Book" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@ id/tv_bookname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}"/>
</LinearLayout>
</layout>
这个是二级页面的代码,下面给出一级页面的代码:
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="book"
type="com.example.databinding.Book"/>
</data>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<include
layout="@layout/second"
app:book="@{book}"/>
</LinearLayout>
</layout>
需要注意的是,在二级页面中同样需要定义布局变量book。
五.自定义BindingAdapter
为了让布局文件能够承担更多的工作,处理更复杂的业务,DataBinding允许我们自定义BindingAdapter,下面我们以ImageView加载网络图片为例来进行说明。
1.导入依赖:implementation 'com.squareup.picasso:picasso:2.71828'
2.添加网络权限
3.编写处理图片的BindingAdapter类,需要注意的是,BindingAdapter中的方法都是静态方法,第一个参数是调用者本身,即ImageView,第二第三个参数是布局文件在调用该方法时传入的参数,代码如下:
代码语言:javascript复制public class ImageViewBindingAdapter {
@BindingAdapter(value = {"image","defaultImage"},requireAll = false)
public static void setImage(ImageView imageView,String imageUrl,int defaultImageResource){
if(!TextUtils.isEmpty(imageUrl)){
Picasso.get()
.load(imageUrl)
.placeholder(R.drawable.ic_launcher_background)
.error(R.drawable.ic_launcher_foreground)
.into(imageView);
}
else{
imageView.setImageResource(defaultImageResource);
}
}
}
requireAll用于告诉DataBinding库这些参数是否都要赋值,默认值为true。
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="imageUrl"
type="String"/>
<variable
name="defaultImageResource"
type="int" />
</data>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<ImageView
android:id="@ id/iv_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@{imageUrl}"
app:defaultImage="@{defaultImageResource}"/>
</LinearLayout>
</layout>
代码语言:javascript复制@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.setImageUrl("https://img.yuanmabao.com/zijie/pic/2023/10/22/zicgwnhplns.jpg");
activityMainBinding.setDefaultImageResource(R.drawable.accept);
}
实现的效果就是当网络url为空的时候,就显示本地图片,否则就显示网络图片。
BindingAdapter中的方法有一个有趣的功能——可选旧值,什么意思呢?就是在某些情况下,我们可能想要得到某个属性的旧值,比如我们在修改padding的时候,想要得到修改前的padding值,以防止方法重复调用。下面给出代码:
代码语言:javascript复制@BindingAdapter("padding")
public static void setPadding(ImageView imageView,int oldPadding,int newPadding){
Log.i("Padding","" oldPadding "," newPadding);
if(oldPadding!=newPadding){
imageView.setPadding(newPadding,newPadding,newPadding,newPadding);
}
}
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="padding"
type="int"/>
</data>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<ImageView
android:id="@ id/iv_url"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:padding="@{padding}"/>
</LinearLayout>
</layout>
需要注意的是,在使用这个功能的时候,方法的参数顺序需要先写旧值,后写新值。
六.双向绑定
1.单项绑定和双向绑定
我们在前面所使用的方式都是单项绑定,例如TextView的android:text属性和book对象的name字段之间的绑定,就是一种单项绑定,绑定后,当name字段发生变化时,TextView会自动更新相应的内容。TextView是一个纯粹的用于展示的控件,它不需要和用户进行交互。而对于其他一些能与用户产生交互的控件,例如EditText,它不仅可以像TextView一样,随着字段的变化自动更新控件中的内容,还可以实现当用户修改EditText控件的内容时,对应的字段也能自动更新,这就是双向绑定。
2.怎么实现双向绑定?
假设要实现一个登录界面,我们需要一个用于输入用户名的EditText控件,一个用于保存用户登录信息的Model类LoginModel,我们希望将EditText和LoginModel中的username进行双向绑定。
第一步,编写LoginModel类:
代码语言:javascript复制public class LoginModel {
private String username;
public LoginModel(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "LoginModel{"
"username='" username '''
'}';
}
}
第二步,编写一个用于存放与实现双向绑定相关的业务逻辑的类,这是实现双向绑定的重点,注意该类需要继承BaseObservable。
代码语言:javascript复制public class MyViewModel extends BaseObservable {
private Context context;
private LoginModel loginModel;
public MyViewModel(Context context){
this.context=context;
loginModel=new LoginModel("jack");
}
@Bindable
public String getUserName(){
return loginModel.getUsername();
}
public void setUserName(String username){
if(username!=null&&!username.equals(loginModel.getUsername())){
loginModel.setUsername(username);
Toast.makeText(context, username, Toast.LENGTH_SHORT).show();
notifyPropertyChanged(BR.userName);
}
}
}
分析以上代码,我们在构造器中对LoginModel进行了实例化,并为该字段编写了Getter和Setter方法,在Getter方法上加上@Bindable注解是为了告诉编译器,我们希望对这个字段进行双向绑定。而Setter方法会在用户编辑EditText中的内容时,被自动调用,我们需要在该方法中对username进行手动更新。需要注意的是,在对字段进行更新前,需要判断新值和旧值是否相同,因为在更新后,我们会调用notifyPropertyChanged()方法通知观察者数据已经更新。观察者在收到通知后,会对setter方法进行调用。因此,如果你没有对新值进行判断,就会引发循环调用的问题。
第三步,设置布局变量。
代码语言:javascript复制@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.setMyViewModel(new MyViewModel(this));
}
第四步,完成双向绑定。
代码语言:javascript复制<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="MyViewModel"
type="com.example.databinding.MyViewModel"/>
</data>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<EditText
android:id="@ id/et_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={MyViewModel.userName}"/>
</LinearLayout>
</layout>
需要注意的是,双向绑定的布局表达式为@={}。
3.使用ObservableField优化双向绑定
实际上,上面的做法存在一些弊端。首先我们的类必须继承自BaseObservable,另外,在getter方法上还要加上@Bindable注解,最后还要在Setter方法中手动调用notifyPropertyChanged()方法通知观察者。那么有没有一种更简单的方法呢?有,那就是ObservableField<T>。它能将普通对象包装成一个可观察的对象,他可以包装各种基本数据类型,集合类型和自定义数据类型。下面给出优化方案:
代码语言:javascript复制public class StudentViewModel {
private Context context;
private ObservableField<Student> studentObservableField;
public StudentViewModel(Context context){
this.context=context;
Student student=new Student();
student.setName("jack");
studentObservableField=new ObservableField<>();
studentObservableField.set(student);
}
public String getName(){
return studentObservableField.get().getName();
}
public void setName(String name){
studentObservableField.get().setName(name);
Toast.makeText(context,name,Toast.LENGTH_SHORT).show();
}
}
可以看到,只是将Student用ObservableField包装了起来,其他的代码很类似。后面的设局布局变量和完成双向绑定和之前的方法一样,就不贴代码了。
七.RecyclerView的绑定机制
第一步,编写RecyclerView的布局文件:
代码语言:javascript复制<?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">
<androidx.recyclerview.widget.RecyclerView
android:id="@ id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
第二步,编写Model类:
代码语言:javascript复制public class Book {
private String title;
private String author;
private String image;
public Book(String title, String author, String image) {
this.title = title;
this.author = author;
this.image = image;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
}
第三步,定义用于处理图片的BindingAdapter:
代码语言:javascript复制public class RecyclerViewImageBindingAdapter {
@BindingAdapter("itemImage")
public static void setImage(ImageView imageView,String imageUrl){
if(!TextUtils.isEmpty(imageUrl)){
Picasso.get()
.load(imageUrl)
.placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_background)
.into(imageView);
}
else{
imageView.setBackgroundColor(Color.BLACK);
}
}
}
第四步,编写RecyclerView中每个item对应的布局文件:
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8" ?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="book"
type="com.example.recyclerview2.Book" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@ id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:itemImage="@{book.image}"/>
<LinearLayout
android:layout_toRightOf="@ id/iv_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@ id/tv_booktitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.title}"/>
<TextView
android:id="@ id/tv_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.author}"/>
</LinearLayout>
</RelativeLayout>
</layout>
第五步,编写RecyclerView.Adapter类:
代码语言:javascript复制public class BookAdapter extends RecyclerView.Adapter<BookAdapter.ViewHolder>{
private List<Book> books;
public BookAdapter(List<Book> books){
this.books=books;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemBinding itemBinding= DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),R.layout.item,parent,false);
return new ViewHolder(itemBinding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Book book = books.get(position);
holder.itemView.setBook(book);
}
@Override
public int getItemCount() {
return books.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder{
public ItemBinding itemView;
public ViewHolder(ItemBinding itemView) {
super(itemView.getRoot());
this.itemView=itemView;
}
}
}
第六步,模拟一些假数据:
代码语言:javascript复制public class RecyclerViewViewModel {
public List<Book> getBooks(){
List<Book> books=new ArrayList<>();
for(int i=0;i<100;i ){
Book book=new Book("android编程" i,"叶坤" i,"https://cdn.pixabay.com/photo/2023/08/11/10/13/goat-8183257_1280.png");
books.add(book);
}
return books;
}
}
第七步,在Activity中配置RecyclerView,并为其添加模拟数据:
代码语言:javascript复制public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityMainBinding activityMainBinding= DataBindingUtil.setContentView(this,R.layout.activity_main);
activityMainBinding.recyclerView.setLayoutManager(new LinearLayoutManager(this));
activityMainBinding.recyclerView.setHasFixedSize(true);
activityMainBinding.recyclerView.setAdapter(new BookAdapter(new RecyclerViewViewModel().getBooks()));
}
}
八.在Fragment中使用DataBinding
由于比较简单,直接给出代码:
代码语言:javascript复制 @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FragmentOneBinding fragmentOneBinding= DataBindingUtil.inflate(inflater,R.layout.fragment_one,container,false);
return fragmentOneBinding.getRoot();
}