Android架构之DataBinding(二)

Jetpack组件系列文章
Android架构之LifeCycle组件
Android架构之Navigation组件(一)
Android架构之Navigation组件(二)
Android架构之Navigation组件(三)
Android架构之Navigation组件(四)
Android架构之ViewModel组件
Android架构之LiveData组件
Android架构之Room组件(一)
Android架构之Room组件(二)
Android架构之WorkManager组件
Android架构之DataBinding(一)
Android架构之DataBinding(二)
Android架构之Paging组件(一)
Android架构之Paging组件(二)
Jetpack与MVVM架构

通过前面的学习,相信大家对DataBinding有了一个初步的认识。接下来让我们去深入的学习下DataBinding。比如说下面这行代码

<TextView
     android:id="@+id/tvAuthor"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@{book.author}"
     android:padding ="@{book.padding}"
     android:textSize="25sp" />

DataBinding是如何将book类中的属性设置到text和padding上面的呢?是通过一个叫做@BindingAdapter注解,接下来要我们学习一下BindingAdapter把。

BindingAdapter

一旦我们在gradle中启用DataBinding库,它会为我们生成绑定所需要的各种类。这其中包含大量针对UI控件的、名为XXXBindingAdapter类。在这些类中包含各种静态方法,并且在这些静态方法前都有@BindingAdapter注解,标签中的别名对应于UI控件在布局文件中的属性。

接下来让我们看一看DataBinding针对android:padding属性所编写的代码:
在这里插入图片描述

在来看DataBinding针对android.text属性所编写的代码:
在这里插入图片描述
DataBinding库以静态方法的形式为UI控件的各个属性绑定了相应的代码。若我们在该UI属性中使用了布局表达式,那么当布局文件被渲染的时候,属性所绑定的静态方法会被自动调用。比如:

<TextView
     android:id="@+id/tvAuthor"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="@{book.author}"
     android:padding ="@{book.padding}"
     android:textSize="25sp" />

上例代码中,当TextView控件被渲染时,android.padding和android.text属性会自动调用ViewBindingAdapter.setPadding()方法TextViewBindingAdapter.setText()方法,将book属性的值设置到控件上面。现在我们知道了BindingApater的作用,那么可以自定义BindingAdapter,扩展其功能。

自定义BindingAdapter

比如有这样一个需求,我们经常会使用ImageView加载来自网络的图片,若加载失败,则让它显示一张默认图片。我们可以使用BindingAdapter来实现这个需求。

准备工作

我们采用Glide库来加载网络图片,在app的build.gradle文件中添加Glide库相关的依赖。

annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'

图片加载需要访问网络,在Manifest文件中加入访问网络的权限

<uses-permission android:name="android.permission.INTERNET"/>

编写处理图片的BindingAdapter类

在该方法中通过Glide加载网络图片。

public class ImageViewBindingAdapter {
    @BindingAdapter("image")
    public static  void setImage(ImageView image,String imgUrl){
        if(!TextUtils.isEmpty(imgUrl)){
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .into(image);
        }
    }
}

需要注意的是: ImageViewBindingAdapter中的方法均为静态方法。第1个参数为调用者本身(也就是当前的UI控件),即ImageView; 第2个参数是布局文件在调用该方法时传递过来的参数。 我们在@BindingAdapter中添加了一个别名image,布局文件正是通过别名来调用该方法的。

在布局文件中调用BindingAdapter

1.首先,需要在布局变量中定义一个String,用于存放网络图片的地址。

	<data>
        <variable
            name="networkImage"
            type="String"/>
    </data>

2.接着,在ImageView中通过别名,即我们在ImageViewBindingAdapter文件中定义好的别名image,来调用静态方法。布局表达式@{}中的参数,则是调用方法时传入的参数

<ImageView
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     app:image="@{networkImage}"
     />

需要注意的是,我们需要在布局文件最外层包含以下命名空间,才能调用自定义@BindingAdapter标签定义的静态方法。

 xmlns:app="http://schemas.android.com/apk/res-auto"

4.在Activity中为布局变量赋值,也就是networkImage

public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setNetworkImage("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3363295869,2467511306&fm=26&gp=0.jpg");
    }

结果如下:
在这里插入图片描述
图片被顺利加载出来了。

方法重载

刚才我们已经自定义BindingAdapter类,让UI控件能够通过简单的属性设置,来加载网络图片。 现在。我们可以通过方法重载,让该静态方法支持显示项目资源文件中的图片,代码如下:

@BindingAdapter("image")
    public static  void setImage(ImageView image,int imageResoure){
        image.setImageResource(imageResoure);
    }

布局文件代码:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="localImage"
            type="int"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:image="@{localImage}"
            />
    </LinearLayout>
</layout>

Activity代码:

public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setLocalImage(R.drawable.ic_launcher_background);
    }
}

这样,BindingAdapter便能够显示项目资源文件中的图片了。

在这里插入图片描述

多参数重载

我们可以上面的两个方法,合并成一个方法,并且将两个参数同时传入方法中。当网络图片地址为空时,则显示imageResource参数所指定的图片.代码如下:

@BindingAdapter(value = {"image","defaultImageResource"},requireAll = false)
    public static  void setImage(ImageView image,String imgUrl,int imageResoure){
        if (!TextUtils.isEmpty(imgUrl)) {
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .into(image);
        }else{
            image.setImageResource(imageResoure);
        }
    }

在@BindingAdapter标签中,方法的别名设置以value={"",""}的形式设置,在该方法中,我设置了两个别名,即通过这两个别名都可以调用该静态方法,变量requireAll用于告诉DataBinding库这些参数是否都要赋值,默认为true,即全部要赋值。 布局文件代码如下:

<ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:image="@{networkImage}"
        app:defaultImageResource="@{localImage}"
            />

当networkImage为空时,ImageVIew会显示locallmage所指定的图片。看到这里,大家应该都有一个疑问,方法是如何区分传入的资源是网络资源还是本地资源呢? DataBinding是根据传递参数的类型来进行区分。比如说:
networkImage是网络图片资源,那么就是String类型,方法就知道它传入的是网络资源图片,localImage是本地类型资源,那么就是int类型,方法就知道它传入的是本地资源图片。

可选旧值

BindingAdapter还有一个强大的功能,覆盖Android原先的控件属性。 比如说, 可以设置在每一个TextView的文本都加上后缀: “-小鑫”。代码如下:

 @BindingAdapter("android:text")
    public  static  void setText(TextView textView,String text){
        textView.setText(text+"-小鑫");
    }
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="networkImage"
            type="String"/>

        <variable
            name="localImage"
            type="int"/>
        <variable
            name="text"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">


        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:image="@{networkImage}"
            app:defaultImageResource="@{localImage}"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25sp"
            android:text="@{text}"/>
    </LinearLayout>
</layout>
public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setLocalImage(R.drawable.ic_launcher_background);
        activityDataBindingBinding.setText("美女是");
    }
}

在这里插入图片描述

对象转换

自动转换对象

当绑定表达式返回Object时,库会选择用于设置属性值的方法。Object会自动转换为所选方法的参数类型。比如下面代码:

<TextView
       android:text='@{userMap["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content" />

注意: @{userMap[“lastName”]} 可替换为 @{userMap.lastName}。
表达式中的userMap对象会返回一个值,该值会自动转换为用于设置android.text特性值的setText(CharSequence)方法中的参数类型。如果参数类型不明确。则必须在表达式中强制转换返回类型。

自定义转换@BindingConversion

与 BindingAdapter 类似 以下方法会将布局文件中所有以@{String}方式引用到的String变量加上后缀"小鑫大美女錒"

 @BindingConversion
    public  static String convertText(String text){
        return text+"小鑫大美女錒";
    }

XML文件

<TextView
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="25sp"
       android:text="@{text}"/>

在这里插入图片描述
需要注意的是,@BindingConversion会将布局文件中所有以@{String}方式引用到的String变量加上后缀"小鑫大美女錒",但是分以下两种情况:

  1. 如果属性是Android自带的属性。 比如说android:text , @BindingAdapter的优先级低于@BindingConversion,会优先调用@BindingConversion注解的方法
  2. 如果属性是自定义的。 比如我们上面列子中的图片加载,image别名,这时候@BindingAdapter的优先级会高于@BindingConversion,会优先调用@BindingAdapter注解的方法。

大家需知道@BindingAdapter和@BindingConversion的区别,特定的场景使用特定的注解。

双向绑定

在前面我们所讲的都属于单向绑定。例如,TextView的android:text与book对象的title字段之间的绑定,就是一种单向绑定。绑定后,当title字段发生变化时,TextView会更新相应的内容。
在这里插入图片描述

TextView是一个纯粹用于展示的控件,不需要与用户交互。对于其他一些能与用户产生交互的控件,例如EditText, 不仅可以实现随着字段的变化自动更新控件中的内容,还可以实现当用户修改EditText中的内容,对应的字段也会随着更新。
在这里插入图片描述

实现双向绑定

假设我们有这样一个需求: 当userName字段改变的时候,EditText会自动更新,EditText改变的时候,userName字段也会自动更新。

编写一个实现双向绑定的类

该类需要继承自BaseObservable,无论是单向绑定还是双向绑定都是观察者模式。BaseObservable是DataBinding库为了方便实现观察者模式而提供的类。

public class TwoWayBindingViewModel  extends BaseObservable {
    private  String username;

    public TwoWayBindingViewModel() {
        username="小鑫";
    }

    @Bindable
    public  String getUserName(){
        return  username;
    }

    public void  setUserName(String userName){
        if(!TextUtils.isEmpty(userName) && !userName.equals(username)){
            username = userName;
            //可以在此处理一些与业务相关的逻辑,例如保存userName字段
            notifyPropertyChanged(BR.userName);
        }
    }
}

userName字段就是我们实现双向绑定的数据流,为该字段写了Getter和Setter字段。需要注意的是:我们在Getter方法前加上了@Bindable标签,意思是告诉DataBinding,我们希望对这个字段进行双向绑定。而Setter方法会在用户编辑EditText中的内容时,会被自动调用,我们需要在该方法中对userName字段进行收到更新。

注意,在对字段更新之前,需要判断新值与旧值是否不同。因为在更新后,我们会调用notifyPropertyChanged()方法通知观察者,数据已经更新。观察者收到通知后,会对Setter方法进行调用。因此,没有对值进行判断,会引发循环调用的问题。

布局文件编写

EditText的布局表达式由@{}b变为@={}即可. @={} 表示(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.jetpack.TwoWayBindingViewModel" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">


        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.userName}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25sp"
            android:text="@{viewModel.userName}"/>

    </LinearLayout>
</layout>
设置布局变量
public class DataBindingActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);
        activityDataBindingBinding.setViewModel(new TwoWayBindingViewModel());
    }
}

运行程序,效果就如上面所说,实现了双向绑定。

使用ObservableField优化双向绑定

实际上,上面的做法存储一些弊端,首先我们的类必须继承自BaseObservable,另外,在Getter方法前还需要加上@Bindable标签,告诉DataBinding我们要绑定该字段。最后,在Setter方法中手动调用notifyPropertyChanged()方法以通知观察者。

有一种更加简单的做法。那就是ObservableField,它能将普通对象包装成一个可观察对象。ObservableField可用于包装各种基本类型、集合数组、自定义类型。 实现代码如下:

{
    private ObservableField<String> userName;

    public TwoWayBindingViewModel() {
        userName = new ObservableField<>();
        userName.set("小鑫");
    }
    
    public  String getUserName(){
        Log.e("true",userName.get());
        return  userName.get();
    }

    public void  setUserName(String name){
        Log.e("true",name);
        userName.set(name);
    }
}

我们只通过ObservableField对象将字段给包装起来,并为字段编写了Getter和Setter方法。运行程序发现,getUserName()方法在程序启动时被自动调用,当用户修改EditText的内容时,setUserName()方法被自动调用。
在这里插入图片描述
DataBinding还提供了可观察集合 ObservableArrayMap对象和ObservableArrayList对象,这里就不再一一详解。步骤更上面使用差不多。

ObservableField与LiveData

我们发现ObservableField的使用方式和作用与LiveData很像。实际上,二者可以替换使用的,二者的区别在于,LiveData与生命周期相关,它通常在ViewModel中使用,并且需要在页面中通过observe()方法对变化进行监听。而双向绑定无须再页面中加入额外的代码,耦合度耕地。

DataBinding在RecycleView中的使用

添加依赖

在app的build.gradle文件添加RecyclerView的依赖

implementation 'androidx.recyclerview:recyclerview:1.1.0'

编写RecycleView的布局文件

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center"
        tools:context=".DataBindingActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycleView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>


    </LinearLayout>
</layout>

编写Model类

Model类也就是我们的数据类,需要传递要页面的数据。

public class Book {
    public  String title;
    public  String author;
    public  String  imgUrl;

    public Book(String title, String author, String imgUrl) {
        this.title = title;
        this.author = author;
        this.imgUrl = imgUrl;
    }

    public Book() {
    }
}

定义加载图片的BindingAdapter

public class ImageViewBindingAdapter {
    @BindingAdapter("image")
    public static  void setImage(ImageView image,String imgUrl){
        if(!TextUtils.isEmpty(imgUrl)){
            //加载图片显示
            Glide.with(image)
                    .load(imgUrl)
                    .placeholder(R.drawable.ic_launcher_foreground)
                    .error(R.drawable.ic_launcher_foreground)
                    .into(image);
        }
    }
   }

编写RecycleView中item的子布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="book"
            type="com.example.jetpack.Book" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".DataBindingActivity">

    <TextView
        android:layout_weight="1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="@{book.title}"
        android:textSize="25sp"/>
    <ImageView
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:image="@{book.imgUrl}"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:text="@{book.author}"/>


    </LinearLayout>
</layout>

编写RecyclerView.Adapter

public class recycleAdapter extends RecyclerView.Adapter<recycleAdapter.ViewHolder> {
    private List<Book> mList = new ArrayList<>();
    private  Context context;
    public recycleAdapter(Context context,List<Book> mList) {
        this.context = context;
        this.mList = mList;
    }

    @NonNull
    @Override
    public recycleAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        RecyItemBinding recyItemBinding = DataBindingUtil.inflate(LayoutInflater.from(context),R.layout.recy_item
        ,parent,false);
        ViewHolder viewHolder = new ViewHolder(recyItemBinding);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull recycleAdapter.ViewHolder holder, int position) {
            holder.binding.setBook(mList.get(position));
    }

    @Override
    public int getItemCount() {
        return mList.size();
    }
    class ViewHolder extends  RecyclerView.ViewHolder{
        public  RecyItemBinding binding;
        public ViewHolder(@NonNull RecyItemBinding itemView) {
            //itemView.getRoot返回的是UI最外层的布局视图
            super(itemView.getRoot());
            binding = itemView;
        }
    }
}

在Activity配置RecyclerView

public class DataBindingActivity extends AppCompatActivity {
    private List<Book> mList = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityDataBindingBinding activityDataBindingBinding = DataBindingUtil.setContentView(this, R.layout.activity_data_binding);

        //模拟数据
        for (int i = 0; i < 10; i++) {
            Book book = new Book();
            book.author = "小鑫"+i;
            book.imgUrl = "https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=3363295869,2467511306&fm=26&gp=0.jpg";
            book.title = "小鑫大美女"+i;
            mList.add(book);
        }
        activityDataBindingBinding.recycleView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
        activityDataBindingBinding.recycleView.setLayoutManager(new LinearLayoutManager(this));
        activityDataBindingBinding.recycleView.setAdapter(new recycleAdapter(this,mList));
    }

}

添加点击事件

1.编写点击RecycleView中item的方法,点击弹出一个Toast

public class EventHandlerListener {
    private Context context;

    public EventHandlerListener(Context context) {
        this.context = context;
    }

	//将当前item中的书籍书籍传递过来了
    public  void onButtonClicked(Book book){
        Toast.makeText(context, book.author, Toast.LENGTH_SHORT).show();
    }
}

2.更改item的布局文件,引入EventHandlerListener类

<variable
        name="listener"
        type="com.example.jetpack.EventHandlerListener" />

3.为LinearLayout添加点击方法,将当前书籍内容传递过去。

<LinearLayout
        android:onClick="@{()->listener.onButtonClicked(book)}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".DataBindingActivity">

4 在adapter将EventHandlerListener类传递要布局中

@Override
    public void onBindViewHolder(@NonNull recycleAdapter.ViewHolder holder, int position) {
            holder.binding.setBook(mList.get(position));
            holder.binding.setListener(new EventHandlerListener(context));
    }

运行程序,效果如下:
在这里插入图片描述

好了,到这里DataBinding就结束了,想更深入了解的,可以去官网看看DataBinding的用法。 不足之处,欢迎大家留言,共同进步,谢谢大家。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值