JetPack文章相关目录
相关文章:
Jetpack:LifeCycle全面解析(包含检测app启动、进入前台、进入后台例子)。
Jetpack:ViewModel使用指南,实现原理详细解析!
Jetpack:LiveData使用指南,实现原理详细解析!
Jetpack:Room超详细使用踩坑指南!
Jetpack:Room数据库升级详解实战!
文章目录
简介
DataBinding可以让布局承担部分原本属于页面的工作,可以使得页面与布局文件之间的耦合程度降低。
DataBinding具有以下几点优势:
- 部分与UI相关的代码可在布局中完成,代码更简洁,可读。
- 不需要使用findViewById()方法。
- 布局文件可以包含简单的业务逻辑。
依赖:需要在模块的 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?) { } }) }
- 自定义
slideProgress
用于设置seekbar的进度 - 定义
InverseBindingAdapter
attribute 设置为slideProgress
返回 seekbar的进度 - 定义配合
InverseBindingAdapter
的BindingAdapter
, 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不难,但是也需要手动去敲一遍,最好看一下生成的代码,为什么是这么写的,知道原理,用起来才会更加有底气!
文章定期更新,也会对已发的文章查缺补漏,有不正确的欢迎留言私信哈!