前言:心中纵是有所盼 严寒没有减 风很冷 我的手已渐蓝
控件查找对于Android开发来说也是一部血泪史,一直为更有效的方案进行了多种方案的研究和探讨。findViewById()
过于繁琐,强制转换不安全;butterkniife
会存在众多臃肿的全局变量的控件,已不再维护;kotlin-android-extensions
通过引入布局可以直接使用资源 id 访问 View,但是也已被废弃了。Google 推出了新的解决方案:ViewBinding 和 DataBinding。
目前 Jetpack 下的 MVVM 架构模式仍然是 Android 领域下的主流发展方向,DataBinding 可以理解为一种工具,它解决了 View 和数据之间的双向绑定,减少模版代码,释放Activity/Fragment,数据绑定空安全。用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。
在模块中启用 ViewBinding 之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。也就是说,视图绑定会替代 findViewById
。可以更轻松地编写可与视图交互的代码。
在 build.gradle
文件中开启 ViewBinding:
android {
buildFeatures {
viewBinding = true
}
}
如果不需要该布局文件生成绑定类,那么可以在该布局文件的根视图中添加属性 tools:viewBindingIgnore="true"
:
<androidx.constraintlayout.widget.ConstraintLayout
tools:viewBindingIgnore="true" >
androidx.constraintlayout.widget.ConstraintLayout>
ViewBinding 提供了三个绑定视图的方法:
// 绑定到视图 view 上
fun <T> bind(view : View) : T
// inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater) : T
// inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
在 Activity 中使用 ViewBinding:
class ViewBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 解析ActivityViewBindingBinding
val binding = ActivityViewBindingBinding.inflate(layoutInflater)
val contentView = binding.root
setContentView(contentView)
// 通过binding对象直接获取到xml中的控件
binding.tvName.text = "苏火火苏火火"
binding.tvName.setOnClickListener {
}
}
}
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户数据显示"/>
androidx.constraintlayout.widget.ConstraintLayout>
启用 ViewBinding 后会在 /build/generated/data_binding_base_class_source_out
目录下生成类,因为被集成进 AndroidStudio 不需要手动编译会实时编译的:
ActivityViewBindingBinding
源码如下:
public final class ActivityViewBindingBinding implements ViewBinding {
private final ConstraintLayout rootView;
public final AppCompatTextView tvName;
private ActivityViewBindingBinding(ConstraintLayout rootView,
AppCompatTextView tvName) {
this.rootView = rootView;
this.tvName = tvName;
}
@Override
@NonNull
public ConstraintLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityViewBindingBinding inflate(LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityViewBindingBinding inflate(LayoutInflater inflater,
ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_view_binding, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityViewBindingBinding bind(View rootView) {
int id;
missingId: {
id = R.id.tv_name;
AppCompatTextView tvName = ViewBindings.findChildViewById(rootView, id);
if (tvName == null) {
break missingId;
}
return new ActivityViewBindingBinding((ConstraintLayout) rootView, tvName);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
主要是在 bind()
方法中,在 rootView 中通过 findChildViewById()
找到对应控件,创建 ActivityViewBindingBinding
类。所以我们能在 ActivityViewBindingBinding
实例中通过 binding.tvName
的形式获取到该控件。
可以理解为 dataBinding 是一种工具,它解决了 View 和数据之间的双向绑定。它有以下几点优势:
每个使用 DataBinding 的模块都需要在 Build.gradle
文件中添加如下配置:
android {
buildFeatures {
dataBinding = true
}
}
自动生成的 DataBinding 类都继承自该类 ViewDataBinding
:
// 返回被绑定的视图对象
View getRoot()
// 在Binding类中设置一个value值
abstract boolean setVariable(int variableId, Object value)
// 解绑绑定
void unbind()
// 添加绑定监听器
void addOnRebindCallback(OnRebindCallback listener)
// 删除绑定监听器
void removeOnRebindCallback(OnRebindCallback listener)
// 使所有的表达式无效,并请求新的重新绑定刷新UI(重置)
abstract void invalidateAll()
ViewBinding 绑定视图的三个方法它都有:
// 绑定到视图 view 上
fun <T> bind(view : View) : T
// inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater) : T
// inflater 解析布局,再绑定到 View 上
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
还提供了 DataBindingUtil
工具类,从 Layout 中创建 DataBinding,不仅可以绑定 Activity 还可以绑定视图内容(View):
// 返回View的binding,如果不存在则创建一个binding
static <T extends ViewDataBinding> T bind(View root)
// 解析一个binding布局,并返回该布局新创建的binding
static <T extends ViewDataBinding> T inflate(LayoutInflater inflater, int layoutId, ViewGroup parent, boolean attachToParent)
// 将给定的布局设置为Activity的内容View,并返回关联的bingding。给定的布局资源不能是布局
static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId)
通过 View 视图获取 binding:
val layoutView = LayoutInflater.from(this).inflate(R.layout.activity_data_binding, null)
val binding = DataBindingUtil.bind<ActivityDataBindingBinding>(layoutView)
Fragment 中使用 DataBindingUtil
:
class HomeFragment : Fragment {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = DataBindingUtil.inflate<FragmentHomeBinding>(inflater, R.layout.fragment_home, container, false)
return binding.root
}
}
Activity 中使用 DataBindingUtil
:
class DataBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityDataBindingBinding>(this, R.layout.activity_data_binding)
}
在布局文件中,选中根布局的标签,按住Alt+回车
,点击Convert to data binding layout
,转换成 dataBinding 布局:
转换后的布局,最外层变成layout
标签,里面包裹了常规标签和常规布局元素。data 标签下定义参数,可以导入相关包或者类。
. 同时 layout 只能包含一个 View 标签,不能直接包含
。
标签的内容即 DataBinding 的数据,data 标签只能存在一个。元素用来声明在此布局使用到的变量和变量类型,以及类引用。不是所有属性都能用 DataBinding
来绑定的。如果一个属性 xxx
,在该类中有 setXxx
方法,我们才能使用 DataBinding
来绑定。比如android:layout_weight
,android:layout_height
就不能使用 DataBinding
来绑定,而 android:paddingLeft
,android:textSize
都是可以的。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.sum.framework.utils.DisplayUtil" />
<variable
name="student"
type="com.sum.common.model.Student" />
<variable
name="activity"
type="com.sum.demo.viewbinding.DataBindingActivity" />
data>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{student.stuName}" />
layout>
android:text="@{user.stuName}
等于 tvName.text = user.getStuName()
这样就将数据和 view 相关联了。
注意:xml 中的 @{}
只做赋值或者简单的三元运算或者判空等不要做复杂运算,否则违背解耦原则。
如何实现 view 和数据的绑定呢?我们只需要让实体类 model 继承 BaseObservable,当字段发生变化,只需要调用 notifyPropertyChanged()
就可以让 UI 刷新。
data class Student(var name: String) : BaseObservable() {
//当使用name字段发生变更后,若想UI自动刷新,
//要求方法名必须以get开头并且标记Bindable注解
//注解才会自动在build目录BR类中生成entry
@Bindable
fun getStuName(): String {
return name
}
fun setStuName(name: String) {
this.name = name
// 手动刷新
notifyPropertyChanged(BR.stuName)
}
}
当使用 name 字段发生变更后,若要UI自动刷新:
BaseObservable
;notify()
函数既可以刷新视图。class DataBindingActivity : AppCompatActivity() {
var mStudent: Student? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1.解析布局
val binding = DataBindingUtil.setContentView<ActivityDataBindingBinding>(this, R.layout.activity_data_binding)
// 2.构建数据
mStudent = Student("姓名")
// 3.给binding设置student数据
binding.student = mStudent
}
```
//更新Student数据
fun updateStudentName() {
mStudent?.setStuName("苏火火~")
}
}
通过 binding.student = mStudent
给 binding 设置数据,点击【更新Name数据】按钮,调用 updateStudentName()
更新 Name 数据,如下图:
BR 类是
BaseObservable
子类中由@Bindable
注解修饰的函数生成;BR 类生成位置在build/generated/source/kapt/debug/com/sum/common/BR.java
;
如果你无法继承可以通过实现接口方式也可以,查看 BaseObservable 实现的接口自己实现即可。
除了 BaseObservable 还有很多其他的都可以拿来使用,都可以让一个对象一条普通的数据成为可观察的数据,只要它发生了变化,与之相关联的观察者就能监听到,从而做出刷新的动作:
BaseObservable,
ObservableBoolean,
ObservableByte,
ObservableChar,
ObservableDouble,
ObservableField,
ObservableFloat,
ObservableInt,
ObservableLong,
ObservableParcelable,
ObservableShort
双向绑定就是当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据。通过表达式使用@=
表达式就可以视图刷新的时候自动更新数据:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={student.stuName}"/>
比单向绑定多了个=
,stuName 数据变更自动更新 EditText,EditText 输入数据也能自动更新 student 中的 stuName 数据。
事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="activity"
type="com.sum.demo.viewbinding.DataBindingActivity" />
data>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_update_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="更新Name数据"
android:onClick="@{()->activity.updateStudentName()}" />
layout>
这里简单模拟点击时调用 Activity 中的方法。你也可以在 onClick 中调用管理类,ViewModel 等方法。
DataBinding 提供了 BindingAdapter 这个注解用于支持自定义属性或者修改原有属性。注解值可以是已有的 xml 属性,例如 android:src
、android:text
等,也可以自定义属性然后在 xml 中使用。
@BindingAdapter(value = ["imageUrl", "radius"], requireAll = false)
fun setImageUrl(view: ImageView, imageUrl: String, radius: Int) {
if (radius > 0) {
view.setUrlRound(imageUrl, radius)
} else {
view.setUrl(imageUrl)
}
}
<ImageView
android:layout_width="match_parent"
android:layout_height="@dimen/dp_120"
app:imageUrl="@{activity.getImageUrl()}"
app:radius="@{DisplayUtil.dpToPx(10)}" />
当 ImageView 控件的 url 属性值发生变化时,dataBinding 就会通过 setImageUrl()
方法,动态改变 ImageView 的相关属性。
BindingAdapter 更为强大的一点是可以覆盖 Android 原生控件属性:
@BindingAdapter("android:text")
fun setText(view: Button, text: String) {
view.text = "$text-改变原生控件属性"
}
<Button
android:id="@+id/btn_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{"button"}' />
当所有 Button 调用 setText() 方法时,数据都会变成$text-改变原生控件属性
。
class ViewDataBinding {
protected void requestRebind() {
synchronized (this) {
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
}
}
}
因为数据绑定或者数据变更刷新 UI 的时候,都会通过 requestRebind()
方法,然后调用 mChoreographer.postFrameCallback(mFrameCallback)
,最后通过 mUIThreadHandler.post(mRebindRunnable)
;等待下一次屏幕绘制的时候才会执行绑定工作,所以我们在显隐藏控制计算宽高的时候都会有个不及时的问题,但是一般情况下我们进行数据的绑定都是没有问题的。
ActivityDataBindingBinding.xml
布局,在编译时会生成ActivityDataBindingBindingImpl.class
我们可以搜索类 debug 跟进解决问题。DataBinding 也支持布局文件中使用,数组、list、set和Map,且在布局文件中都可以通过 list[index]
的形式获取元素,因为 xml 的特性,在声明 List
的数据类型时,需要使用尖括号的转义字符:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="java.util.List" />
<import type="java.util.Set" />
<import type="java.util.Map" />
<import type="android.util.SparseArray" />
<variable
name="array"
type="String[]" />
<variable
name="list"
type="List<String>" />
<variable
name="map"
type="Map<String, String>" />
<variable
name="set"
type="Set<String>" />
<variable
name="sparse"
type="SparseArray<String>" />
<variable
name="index"
type="int" />
<variable
name="key"
type="String" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:text="@{array[1]}" />
<TextView
android:text="@{sparse[index]}" />
<TextView
android:text="@{list[index]}" />
<TextView
android:text="@{map[key]}" />
<TextView
android:text="@{map['苏火火']}" />
<TextView
android:text='@{set.contains("xxx")?"苏火火":key}' />
LinearLayout>
DataBinding 在 xml 中数据绑定支持的语法表达式也是非常丰富的,支持在布局文件中使用以下运算符,表达式和关键字:
目前不支持以下操作
<data>
<variable
name="isLeft"
type="boolean" />
data>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@{isLeft ? @dimen/dp_20:@dimen/dp_12}"
android:textAllCaps="false" />
注意:控件的宽高不能使用 DataBinding 动态绑定。
避免空指针异常
DataBinding 也会自动帮助我们避免空指针异常 例如,如果 “@{student.age}” 中 student 为 null 的话,student.age 会被赋值为默认值 null,而不会抛出空指针异常。
空合并运算符 ?? 会取第一个不为 null 的值作为返回值:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{student.stuName ?? student.age}" />
//等价于
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{student.stuName != null ? student.stuName : student.age}" />
对于 include 的布局文件,是支持通过 dataBinding 来进行数据绑定,需要在 include 的布局中使用 layout 标签,声明需要使用到的参数。
view_include.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.sum.common.model.Student" />
<variable
name="student"
type="Student" />
data>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:text="@{student.stuName}" />
layout>
在主布局文件中将相应的参数传递给 include 布局,从而使两个布局文件之间共享同一个参数:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.sum.common.model.Student" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_binding_include"
bind:student="@{student}" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
dataBinding 也支持 ViewStub 布局,在主布局文件中引用 viewStub 布局:
view_stub.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<variable
name="student"
type="com.sum.common.model.Student" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewStub
layout="@layout/layout_binding_include"
bind:student="@{student}"
android:layout="@layout/layout_binding_viewstub" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
如果需要为 ViewStub 绑定变量值,则 ViewStub 文件一样要使用 layout 标签进行布局,主布局文件使用自定义的 bind 命名空间将参数传递给 ViewStub,获取到 ViewStub 对象:
val viewStubBinding = binding.layoutBindingViewstub.viewStub?.inflate()
如果在 xml 中没有使用 bind:student="@{student}"
对 ViewStub 进行数据绑定,则可以在 ViewStub Inflate() 时再绑定,此时需要为 ViewStub 设置 setOnInflateListener
回调函数,在回调函数中进行数据绑定:
val viewStub = binding.layoutBindingViewstub.viewStub
viewStub?.setOnInflateListener { stub, inflated -> //如果在 xml 中没有使用 bind:student="@{student}" 对 viewStub 进行数据绑定
//那么可以在此处进行手动绑定
val viewStubBinding: LayoutBindingViewstubBinding? = DataBindingUtil.bind(stub)
viewStubBinding?.student = mStudent
}
使用 DataBinding 实现单向双向绑定时,model 必须要继承 BaseObservable 或者使用 ObservableField,还要添加 @Bindable 注解、调用 notifyPropertyChanged()
手动刷新,这样做代码入侵性比较强。
上一篇文章中介绍 LiveData,它实现数据驱动的,它包裹的 Student 并没有继承 BaseObservable。LiveData 可以代替 BaseObservable,ObservableField等,并且它还自动具备生命周期管理。不用侵入式的修改数据实体类了,直接使用 LiveData,同样支持 DataBinding 的数据绑定。
在 Activity 中实现:
// 使用LiveData作为数据绑定来源,要设置LifecycleOwner
binding.lifecycleOwner = this
val viewModel = ViewModelProvider(this)[MainViewModel::class.java]
// 给布局设置ViewModel参数
binding.vm = viewModel
布局文件增加 ViewModel 参数:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="vm"
type="com.sum.demo.viewmodel.MainViewModel" />
data>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData}" />
layout>
LifecycleOwner
;相对于 findViewById、butterknife、kotlin-extension 而言,ViewBinding 和 DataBinding 都生成可用于直接引用视图的绑定类,能兼容 Kotlin、Java,使用更方便,减少Null安全,类型安全等问题。(无效 View ID 而引发 Null 异常,控件类型转换异常等)
但是它们也增加编译时间,因为 ViwBinding 是在编译时生成的,会产生额外的类,增加包的体积;include 的布局文件无法直接引用,需要给 include 给 id 值,然后间接引用。
如果我们需要数据和 view 之间有联动绑定效果则可以使用 DataBinding,如果不需要数据绑定则选择 ViewBinding。整体来说 ViewBinding 与 DataBinding 的优点还是远远大于缺点的,所以推荐使用。
源码地址: https://github.com/suming77/SumTea_Android
好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!
本人水平有限,文章难免会有错误,请批评指正,不胜感激 !