Jetpack:DataBinding超详细指南-Kotlin版!

11 篇文章 0 订阅

JetPack文章相关目录

相关文章:
Jetpack:LifeCycle全面解析(包含检测app启动、进入前台、进入后台例子)。
Jetpack:ViewModel使用指南,实现原理详细解析!
Jetpack:LiveData使用指南,实现原理详细解析!
Jetpack:Room超详细使用踩坑指南!
Jetpack:Room数据库升级详解实战!


简介

DataBinding可以让布局承担部分原本属于页面的工作,可以使得页面与布局文件之间的耦合程度降低。

DataBinding具有以下几点优势:

  1. 部分与UI相关的代码可在布局中完成,代码更简洁,可读。
  2. 不需要使用findViewById()方法。
  3. 布局文件可以包含简单的业务逻辑。

依赖:需要在模块的 build.gradle 文件中将 dataBinding 构建选项设置为 true,如下所示:

android {
    ...
    buildFeatures {
        dataBinding true
    }
}
//开启kapt
plugins {
    ...
    id 'kotlin-kapt'
}

注意

可以同时使用viewBinding和dataBinding,布局里面使用layout标签,则生成ViewDataBinding类型,否则生成ViewBinding类型的绑定类

DataBinding简单使用

  • 根据简介的描述在build.gradle开启dataBinding

  • 修改布局文件
    在布局文件最外层添加<layout>标签作为根节点。可以手动修改添加<layout>标签;也可以将鼠标移动到原布局跟节点位置,单击浮现的灯泡按钮,然后选择Convert to databinding layout选项,由编译器帮忙进行转化。如下图所示:
    在这里插入图片描述
    修改完成后的布局如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    
  • 实例化布局文件
    常规的实例化布局文件方法通过Activity.this.setContentView()方法实例布局文件,然后通过findViewById()方法找到对应的UI控件并使用。使用DataBinding之后可以直接获取对应的Binding类,然后将跟布局设置进布局文件即可;或者使用DataBindingUtil.setContentView()方法直接设置布局文件,且该方法也会返回对应的Binding
    举个🌰

    	private var _binding: ActivityDatabindingBaseBinding? = null
        val binding: ActivityDatabindingBaseBinding
            get() = _binding!!
    	//一、直接通过Binding获取Binding类,并且设置布局
    	private fun initBinding() {
            _binding = ActivityDatabindingBaseBinding.inflate(layoutInflater)
            setContentView(binding.root)
        }
    	//二、使用DataBindingUtil直接设置布局
    	private fun initBinding() {
            _binding = DataBindingUtil.setContentView(this, R.layout.activity_databinding_base)
        }
    
    

    通过对应的Binding类可以直接方法,布局文件声明的Id类,会将原布局文件声明的id命名,转换为驼峰命名法。

    举个🌰

    //布局文件的id
    <TextView
        android:id="@+id/tv_video_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    //使用binding访问
    binding.tvVideoName.text = "access ui controls through binding"
    
  • 将数据传递到布局文件,由布局文件负责进行数据绑定

    databinding出现就希望减轻,UI曾对应数据绑定的逻辑,分层更加清晰。所以需要将数据传递到布局文件中,由布局文件负责数据的绑定。

    首先声明一个实体类VideoEntity

    data class VideoEntity(
        val videoName: String? = "迪迦·奥特曼",
        val videoIntroduction: String? = "《迪迦·奥特曼》(ウルトラマンティガ、Ultraman Tiga),是日本圆谷株式会社拍摄的特摄电视剧。是“平成系奥特曼”系列首作,平成三部曲的首作,是奥特曼系列自1980年的 《爱迪·奥特曼》后沉寂数年迎来的重生。于1996年(平成8年)9月7日至1997年(平成9年)8月30日在JNN日本新闻网播放 [1]  ,共52话。",
        val videoStarring: String? = "长野博;吉本多香美;高树零;增田由纪夫;影丸茂树;古屋畅一;川地民夫;石桥慧;二又一成",
        val videoImageUrl: String? = "https://img1.baidu.com/it/u=2288494528,306759139&fm=253&fmt=auto&app=120&f=JPEG?w=584&h=378",
        @DrawableRes val localImage: Int = R.drawable.ic_launcher_background
    )
    

    将实体类VideoEntity传入到布局文件中,<layout>标签下创建<data>标签(通过编译器直接转换databinding layout 的话,会自动生成<data>标签),<data>标签下创建<variable>标签,指定对象的路径以及对应的名称

    • type实体类的路径
    • name可自定义名称

    举个🌰

    <?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"
        xmlns:tools="http://schemas.android.com/tools">
        <data>
            <!--        实体类-->
            <variable
                name="videoEntity"
                type="com.zxf.jetpackrelated.databinding.simpleUse.VideoEntity" />
        </data>
        <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
                <TextView />
                <TextView />
                <TextView />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    

    在activity中,DataBinding直接生成了对应的setter方法。所以可以直接使用setVideoEntity将对象传递给布局类。如下所示:

    val videoEntity = VideoEntity()
    //绑定实体类
    binding.videoEntity = videoEntity
    
  • 绑定布局变量
    将对象传递进去直接,就可以通过@{}直接在布局文件中将对应的变量绑定给控件了

    <TextView
        android:text="@{videoEntity.videoName}" />
    
    <TextView
        android:text="@{videoEntity.videoIntroduction}" />
    
    <TextView
        android:text="@{videoEntity.videoStarring}" />
    

    @{}中也可以直接使用静态方法

    <TextView
        android:text="@{String.valueOf(1)}" />
    

DataBinding响应事件

通过Button控件,演示DataBinding如何响应onClick事件。

  • 创建布局文件转为DataBinding布局

    <?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"
        xmlns:tools="http://schemas.android.com/tools">
        <data>
            
        </data>
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <Button/>
            <Button />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>
    
  • 声明一个EventHandleListener专门处理点击事件

    	class EventHandleListener(private val videoEntity:VideoEntity) {
    
            fun onButtonClick1(view: View) {
                Toast.makeText(context, "click1", Toast.LENGTH_SHORT).show()
            }
    
            fun onButtonClick2(view: View) {
                Toast.makeText(context, "click2", Toast.LENGTH_SHORT).show()
            }
        }
    
  • 布局中定义EventHandleListener变量,并将点击事件传递进button。通过双冒号语法::进行调用

    <variable
        name="eventHandlerLayout"
        type="com.zxf.jetpackrelated.databinding.simpleUse.SimpleDataBindingActivity.EventHandleListener"/>
        
        
    <Button
    	android:onClick="@{eventHandlerLayout::onButtonClick1}"
    />
    <Button 
    	android:onClick="@{eventHandlerLayout::onButtonClick2}"
    />
    
  • activity中将EventHandleListener传递给布局文件

    binding.eventHandler = EventHandleListener(videoEntity)
    

DataBinding二级页面的绑定

简单来说就是怎么将,数据传递到通过<include>标签包裹的二级页面呢?

下面就将上面的按钮响应事件通过<include>包裹一下,将EventHandleListener进行一个二级传递。

  • 在一级页面定义<include>,将上面的按钮布局包裹进来

    <include
    	android:id="@+id/in_bt"
    	layout="@layout/layout_databinding_base" />
    
  • 在一级页面同样声明一下EventHandleListener变量,可以通过二级页面声明的EventHandleListener的name,将对象直接传递给二级页面

    <variable
        name="eventHandler"
        type="com.zxf.jetpackrelated.databinding.simpleUse.SimpleDataBindingActivity.EventHandleListener" />
    
    <include
        android:id="@+id/in_bt"
        layout="@layout/layout_databinding_base"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:eventHandlerLayout="@{eventHandler}"/>
    

BindAdapter简单原理分析

当启用databinding之后,会生成大量的类,其中包括针对UI的各种命名为XXXBindAdapter的类。

截取一下TextViewBindingAdapter类的部分源码。源码展示了DataBinding库针对android:text属性所编写的代码

    @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);
    }

在布局文件中通过@{}绑定的text,最终通过生成的BindingImpl类,调用了对应的setText静态方法。

所以总的来说,在布局文件里面绑定的数据,可以调用到BindingAdapter修饰的静态方法

自定义BindAdapter

仅官方提供的BindAdapter肯定是不够的,所以需要自定义BindAdapter,实现更多的更复杂的业务关系。

下面展示使用ImageView自定义BindAdapter。

  • BindAdapter注解介绍

    @Target(ElementType.METHOD)
    public @interface BindingAdapter {
        String[] value();
        boolean requireAll() default true;
    }
    
    • value 即分配给布局文件中使用的名字
    • requireAll 如果需要一下子定义多个value,用于区别是否所有的都需要,默认为true,如果为true,则布局文件中,一旦使用了其中一个其他的也必须要声明,不然编译不通过。
  • 使用glide进行图片加载

    添加依赖和网络访问权限

        implementation 'com.github.bumptech.glide:glide:4.12.0'
        annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
        
        <uses-permission android:name="android.permission.INTERNET" />
    
  • 定义ImageView的BindAdapter类

    需要注意的是,BindingAdapter中的方法均为静态方法。第1个参数为调用者本身,即ImageView;第2个参数是布局文件在调用该方法时传递过来的参数

    对于Kotlin来说,只需要将函数声明为顶级函数,就对应java的静态方法了。

    @BindingAdapter("image")
    fun setImage(imageView: ImageView, imageUrl: String?) {
        if (imageUrl.isNotNullOrEmpty()) {
            Glide.with(imageView)
                .load(imageUrl)
                .into(imageView)
        } else {
            imageView.setBackgroundColor(Color.BLUE)
        }
    }
    //isNotNullOrEmpty 是自定义的扩展函数
    fun CharSequence?.isNotNullOrEmpty(): Boolean {
        return !isNullOrEmpty()
    }
    
  • 布局文件中调用BindingAdapter类

    方便期间,直接使用上面的VideoEntity类了,里面有对应的图片地址字段videoImageUrl

    然后使用image绑定布局

    <ImageView
        app:image="@{videoEntity.videoImageUrl}" />
    
  • 运行结果(加上之间简单使用的整体结果)
    在这里插入图片描述

  • BindAdapter方法重载

    如果想网络图片加载url不存在则加载本地资源呢?多传递一个参数就好了,怎么传呢?举个🌰

    /**
     * 方法的参数以value={"",""}的形式存在
     * 变量requireAll用于告诉DataBinding库这些参数是否都要赋值,默认值为true,即全部需要赋值,这里写成false;
     * 如果设置为true,则对应的属性必须在xml一起生命,否则编译报错
     */
    @BindingAdapter(value = ["image", "defaultImageRes"], requireAll = false)
    fun setImage2(imageView: ImageView, imageUrl: String?, @DrawableRes imageRes: Int) {
        if (imageUrl.isNotNullOrEmpty()) {
            Glide.with(imageView)
                .load(imageUrl)
                .into(imageView)
        } else {
            imageView.setImageResource(imageRes)
        }
    }
    
  • BindAdapter可选旧值

    在某些情况下,可能希望在方法中得到该属性之前的值。假如,在修改控件的padding时,可能希望得到修改前的padding,以防止方法重复调用。

    举个🌰

    /**、
     * 需要注意的是,使用可选旧值时,方法中的参数顺序需要先写旧值,后写新值。即oldPadding在前,newPadding在后。
     */
    @BindingAdapter(value = ["padding"])
    fun setPadding(view: View, oldValue: Int, newValue: Int) {
        LogUtil.d("------------paddingValueLog: oldValue:$oldValue newValue:$newValue")
        if (oldValue != newValue) {
            view.setPadding(newValue, newValue, newValue, newValue)
        }
    }
    

    需要注意的是,使用可选旧值时,方法中的参数顺序需要先写旧值,后写新值。即oldPadding在前,newPadding在后。

    绑定的代码就不写了,做了个例子,默认设置padding为30,点击时变为60,看一下控制台打印的数据

    2021-10-12 11:10:06.690 12768-12768/com.zxf.jetpackrelated D/[setPadding(CustomBindingAdapterKt:74)]: ------------paddingValueLog: oldValue:0 newValue:30
    2021-10-12 11:10:08.252 12768-12768/com.zxf.jetpackrelated D/[setPadding(CustomBindingAdapterKt:74)]: ------------paddingValueLog: oldValue:30 newValue:60
    

    很明显看到,程序启动时,padding由0变为30。单击button后,程序由30变为60.

实现双向绑定

上面所有写的都只是简单的绑定,当数据变化后,目前是没有通知到UI进行刷新,同时UI刷新后,数据也没有变化。

下面通过EditText和TextView控件简单实现双向绑定。

  • 使用BaseObservable实现双向绑定

    • 编写实体类DisplayEntity

      class DisplayEntity(
          var displayStr: String
      )
      
    • 编写一个用于存放与实现双向绑定相关的业务逻辑的类,继承BaseObservable

      class TwoBindingEntityObservable : BaseObservable() {
          private val displayEntity: DisplayEntity = DisplayEntity("双向绑定测试")
      
          /**
           * 在getter 方法上面增加@Bindable  告诉编译器希望对这个字段进行双向绑定
           * 在xml中直接使用displayStr 进行绑定
           */
          @Bindable
          fun getDisplayStr(): String = displayEntity.displayStr
      
          /**
           * setter 方法在用户编译edittext的时候被自动调用,需要在这里面对displayStr字段进行手动更新
           * 调用notifyPropertyChanged方法通知观察者数据已变更
           * 需要对值进行判断否则会产生循环调用的问题
           */
          fun setDisplayStr(displayStr: String) {
              //需要对值进行判断否则会产生循环调用的问题
              if (displayStr.isNotNullOrEmpty() && displayStr == displayEntity.displayStr) {
                  return
              }
              displayEntity.displayStr = displayStr
              notifyPropertyChanged(BR.displayStr)
          }
      }
      

      初始化时为字段displayStr设置了默认值,接着为该字段写了Getter和Setter方法。注意,在Getter方法前加上了@Bindable标签,告诉编译器,希望对这个字段进行双向绑定。而Setter方法会在用户编辑EditText中的内容时,被自动调用,需要在该方法内对userName字段进行手动更新。

      notifyPropertyChanged()是BaseObservable类中的一个方法,会通知UI进行刷新。

    • 设置布局以及activity(注意::双向绑定使用@={})

      <variable
          name="entityObservable"
          type="com.zxf.jetpackrelated.databinding.twoWayBinding.TwoBindingEntityObservable" />
          
      <EditText
          android:text="@={entityObservable.displayStr}"/>
      
      <TextView
      	android:text="@={entityObservable.displayStr" />
      
      binding.entityObservable = TwoBindingEntityObservable()
      
    • 这样在编辑EditText的时候,对应的TextView也会展示对应的数据,实现了双向绑定

  • 使用ObservabIeFieId更简单的实现双向绑定

    • 创建ObservabIeFieId

      class TwoBindingEntityObservable3 {
          val displayEntityField = ObservableField("双向绑定测试-use observableField")
      }
      
    • 设置布局和activity

      <variable
      	name="entityObservable3"
      	type="com.zxf.jetpackrelated.databinding.twoWayBinding.TwoBindingEntityObservable3" />
      	
      <EditText
          android:text="@={entityObservable3.displayEntityField}"/>
      
      <TextView
      	android:text="@={entityObservable3.displayEntityField" />
      
      binding.entityObservable = TwoBindingEntityObservable3()
      
    • 就可以了,是不是很简单!

      databinding默认实现了一系列实现Observable接口的字段类型,可以选择使用

      BaseObservable,
      ObservableBoolean,
      ObservableByte,
      ObservableChar,
      ObservableDouble,
      ObservableField<T>,
      ObservableFloat,
      ObservableInt,
      ObservableLong,
      ObservableParcelable<T extends Parcelable>,
      ObservableShort,
      ViewDataBinding
      

自定义InverseBindingAdapter

同理可以自定义BindAdapter实现更加个性化的绑定(通知到UI),那么一样可以自定义当UI变化进行反绑(通知到数据)

这时候就用到了InverseBindingAdapter。

  • InverseBindingAdapter解析
    • attribute 属性值(必填) 可以对应到BindingAdapter 的 value
    • event 非必填, 默认值 属性值 + AttrChanged后缀

简单解释一下怎么使用,InverseBindingAdapter修饰的静态方法,会在数据变更之后进行被回调,返回控件的数据,所以他对应一个@BindingAdapter方法来实现event的属性. 用来通知数据什么时候变更。直接看实践代码吧,更加清晰一点。

创建一个SeekBar,实时显示进度到TextView

  • 创建InverseBindingAdapter和BindingAdapter

    @BindingAdapter(value = ["slideProgress"])
    fun setSeekProgress(seekbar: SeekBar, slideProgressOld: Int, slideProgressNew: Int) {
        if (slideProgressOld == slideProgressNew) {
            return
        }
        seekbar.progress = slideProgressNew
    }
    
    /**
     *  InverseBindingAdapter 配合  BindingAdapter  一起使用
     *
     * event 非必填, 默认值 属性值 + AttrChanged后缀
     */
    @InverseBindingAdapter(attribute = "slideProgress", event = "slideProgressAttrChanged")
    fun getSeekProgress(seekbar: SeekBar) = seekbar.progress
    
    @BindingAdapter(value = ["slideProgressAttrChanged"])
    fun setSeekProgress(seekbar: SeekBar, inverseBindingListener: InverseBindingListener?) {
        seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                inverseBindingListener?.onChange()
            }
    
            override fun onStartTrackingTouch(seekBar: SeekBar?) {
    
            }
    
            override fun onStopTrackingTouch(seekBar: SeekBar?) {
    
            }
    
        })
    }
    
    1. 自定义slideProgress用于设置seekbar的进度
    2. 定义InverseBindingAdapter attribute 设置为 slideProgress 返回 seekbar的进度
    3. 定义配合InverseBindingAdapterBindingAdapter, value 为 InverseBindingAdapter 的 event,第二个参数为 InverseBindingListener ,当数据发生之后,调用onChange()。
  • 创建可观察进度

    val observableProgress = ObservableInt(50)
    
  • xml文件绑定

    <?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="observableProgress"
                type="androidx.databinding.ObservableInt" />
        </data>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <SeekBar
                app:slideProgress="@={observableProgress}" />
    
    
            <TextView
                android:text="@{String.valueOf(observableProgress)}" />
        </LinearLayout>
    </layout>
    
  • 设置数据

    binding.observableProgress = observableProgress
    

InverseBindingAdapter简单分析

上面的方法是不是有点蒙,简单分析一下都是怎么做的。看生成的BindingImpl文件

在布局绑定的时候会调用到executeBindings方法

    @Override
    protected void executeBindings() {
        if ((dirtyFlags & 0x2L) != 0) {
            //这里的自动绑定了  自定义的BindingAdapter 的 setSeekProgress  传进去了  mboundView1slideProgressAttrChanged
            com.zxf.jetpackrelated.databinding.CustomInverseBindingAdapterKt.setSeekProgress(this.mboundView1, mboundView1slideProgressAttrChanged);
        }
    }

看一下 mboundView1slideProgressAttrChanged

    private androidx.databinding.InverseBindingListener mboundView1slideProgressAttrChanged = new androidx.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // InverseBindingAdapter  的getSeekProgress
            int callbackArg_0 = com.zxf.jetpackrelated.databinding.CustomInverseBindingAdapterKt.getSeekProgress(mboundView1);
            boolean observableProgressJavaLangObjectNull = false;
            // observableProgress
            androidx.databinding.ObservableInt observableProgress = mObservableProgress;
            observableProgressJavaLangObjectNull = (observableProgress) != (null);
            if (observableProgressJavaLangObjectNull) {
                //设置进 ObservableInt  ObservableInt内部会观察 然后调用 notify 最后由进行ui绑定
                observableProgress.set(((int) (callbackArg_0)));
            }
        }
    };

所以当,onChange被调用的时候,调用了InverseBindingAdapter 修饰的 getSeekProgress。将获取的值传入进入ObservableInt,ObservableInt内部会观察 然后调用 notify 最后由进行ui绑定(还是调用到executeBindings方法)

总结

常用的基本都在上面了,databinding不难,但是也需要手动去敲一遍,最好看一下生成的代码,为什么是这么写的,知道原理,用起来才会更加有底气!
文章定期更新,也会对已发的文章查缺补漏,有不正确的欢迎留言私信哈!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值