DataBinding的意义
1、布局文件通常只负责UI控件的布局工作,页面中通过代码对控件需要进行各种操作,承担了绝大部分的工作量
2、DataBinding让布局文件承担了部分原本属于页面的工作,也使得布局文件和页面的耦合度进一步降低
3、使得UI控件能够直接合数据模型中的字段绑定,甚至能响应用户的交互。方便实现MVVM
一、DataBinding简单使用
1、启动DataBinding
在模块下的build.gradle文件中,启动dataBinding。
android {
dataBinding {
enabled = true
}
}
高版本采用
android {
buildFeatures {
dataBinding = true
}
}
2、将普通布局文件转换为DataBinding布局文件
可在布局文件根标签右键中使用IDE的功能自动转换
转换后如下:
<?xml version="1.0" encoding="utf-8"?>
<!--DataBinding布局根标签是layout标签-->
<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="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HelloWorld"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
3、实例化布局文件
将Activity的setContentView(R.layout.xxx)改成DataBindingUtil.setContentView()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//实例化DataBinding对象
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
}
4、将数据传递到布局文件
第3步后,布局文件转化为了实例对象,那么可以为该实例绑定数据,并在布局文件中使用数据
布局文件中使用数据:
<?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>
<!--绑定book对象,类型为com.breeze.jetpackstudy.model.Book-->
<variable
name="book"
type="com.breeze.jetpackstudy.model.Book" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!--android:text="@{book.name}",TextView的text属性绑定book对象的name成员-->
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--android:text="@{book.author}",绑定book对象的author成员-->
<TextView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{book.author}"
android:layout_marginTop="20dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
为布局文件实例,绑定book数据对象,DataBinding为了使用方便,生成了set方法,如本例中binding.book可直接调用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.book = Book("DataBinding Study", "Breeze")
}
5、运行结果
二、在布局文件中引用静态类
有许多场景我们需要对数据的展示做一些处理,比如网络返回的数据bean字段是数字,而展示时要转成对应文案。以往的做法是在代码中去做转化,后重新设置到view上显示。使用databinding可以在布局文件中直接使用静态类转化。
静态工具类:
class BookRatingUtil {
companion object {
@JvmStatic
fun getRatingString(rate : Int) =
when (rate) {
0 -> "零星"
1 -> "一星"
2 -> "二星"
3 -> "三星"
4 -> "四星"
5 -> "五星"
else -> ""
}
}
}
布局文件:
<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="book"
type="com.breeze.jetpackstudy.model.Book" />
<!--import引用工具类-->
<import type="com.breeze.jetpackstudy.BookRatingUtil"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{book.author}"
android:layout_marginTop="20dp" />
<!--使用工具类对book.rate做转化-->
<TextView
android:id="@+id/rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/author"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{BookRatingUtil.getRatingString(book.rate)}"
android:layout_marginTop="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
三、二级页面的绑定
布局文件较复杂时,难免使用include标签将布局进行拆分。此时使用DataBinding该如何传递数据呢?
一级页面如下:
<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.breeze.jetpackstudy.model.Book" />
<import type="com.breeze.jetpackstudy.BookRatingUtil"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--给二级页面传递数据-->
<include layout="@layout/include_layout"
app:book="@{book}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
二级页面接收传递的数据:
<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="book"
type="com.breeze.jetpackstudy.model.Book" />
<import type="com.breeze.jetpackstudy.BookRatingUtil"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondLevelActivity">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{book.author}"
android:layout_marginTop="20dp" />
<TextView
android:id="@+id/rate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/author"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{BookRatingUtil.getRatingString(book.rate)}"
android:layout_marginTop="20dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
四、DataBinding响应事件
1、布局内容如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="eventHandler"
type="com.breeze.jetpackstudy.EventHandler" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--注意这里传的是方法引用,没有带括号或者参数,写法可以是eventHandler.onBtnClick或eventHandler::onBtnClick,个人觉得::更合适一点-->
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me"
android:onClick="@{eventHandler.onBtnClick}"/>
</LinearLayout>
</layout>
2、EventHandler类
class EventHandler(private val context : Context) {
//这里的方法形参view:View不能省略,要和OnClickListener保持相同的方法参数
fun onBtnClick(view: View) {
Toast.makeText(context, "I am Clicked", Toast.LENGTH_SHORT).show()
}
}
3、绑定
class EventActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityEventBinding>(this, R.layout.activity_event)
binding.eventHandler = EventHandler(this)
}
}
五、自定义BindingAdapter
以为ImageView增加自定义属性为例
1、编写ImageViewBindingAdapter类,注意方法需要写成静态方法
class ImageViewBindingAdapter {
companion object {
@JvmStatic
@BindingAdapter("image")
fun setImage(imageView : ImageView, imageUrl : String) {
if (!TextUtils.isEmpty(imageUrl)) {
Picasso.with(imageView.context)
.load(imageUrl)
.into(imageView)
}
}
//这里支持重载,并且支持配多个参数,上面的image属性,在下面多个参数的情况出现,实测这种情况执行的是多个参数的这个
@JvmStatic
@BindingAdapter(value = ["image", "defaultImageResource"], requireAll = false)
fun setImage(imageView: ImageView, imageUrl: String, imageResource : Int) {
Log.e("Breeze", "setImage, image defaultImageResource")
if (!TextUtils.isEmpty(imageUrl)) {
Picasso.with(imageView.context)
.load(imageUrl)
.into(imageView)
} else {
imageView.setImageResource(imageResource)
}
}
}
}
2、布局中使用
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="networkImage"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--这里直接使用app:image属性-->
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image="@{networkImage}"/>
</LinearLayout>
</layout>
3、绑定networkImage
val binding = DataBindingUtil.setContentView<ActivityBindingadapterBinding>(this, R.layout.activity_bindingadapter)
binding.networkImage = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1606037072884&di=425fa1754c78e7288acf2bf8797750ba&imgtype=0&src=http%3A%2F%2Fbpic.588ku.com%2Felement_origin_min_pic%2F00%2F89%2F93%2F6156ee970479161.jpg"
4、DataBinding自身定义的BindingAdapter
ViewBindingAdapter源码,定义了view属性相关的绑定
ImageViewBindingAdapter源码:
这些方法在什么时候调用的咱后续再分析~
六、BaseObservable实现双向绑定
上面的案例中实现的都是单向绑定(在数据发生变化时,能做到界面跟着变化,但没有做到界面发生变化时,数据跟着变化)。双向绑定要达到的效果便是除了数据影响界面,界面变化也要使得数据发生变化。比如EditText输入内容时,绑定的数据bean要跟着变化。使用BaseObservable便能实现这一点
class LoginModel(var userName : String)
//固定写法,必须继承BaseObservable
class TwoWayBindingViewModel : BaseObservable() {
private val loginModel : LoginModel = LoginModel("Breeze")
//固定写法,get方法必须有Bindable注解
@Bindable
fun getUserName() : String {
return loginModel.userName
}
fun setUserName(userName : String) {
//关键点,此处必须判断数据是否发生变化,否则会发生死循环。当界面发生变化时,会回调到这里
if (userName != loginModel.userName) {
loginModel.userName = userName
Log.e("ZXX", "${loginModel.userName}")
//固定写法,这里要调用notifyPropertyChanged
notifyPropertyChanged(BR.userName)
}
}
}
布局中使用
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="loginModel"
type="com.breeze.jetpackstudy.model.TwoWayBindingViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--这里使用@=进行双向绑定-->
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@={loginModel.userName}"/>
</LinearLayout>
</layout>
Activity注入数据
class TwoWayActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityTwowayBinding>(this, R.layout.activity_twoway)
binding.loginModel = TwoWayBindingViewModel()
}
}
七、使用ObservableField-更优雅的双向绑定
使用BaseObservable实现双向绑定时,多处写到"固定写法",这是一种比较不优雅的实现方式,因为容易出错。
在布局文件和Activity中绑定数据代码不变情况下,修改TwoWayBindingViewModel实现为以下方式,使用ObservableField将数据bean进行包装
class TwoWayBindingViewModel {
private val loginObservableField : ObservableField<LoginModel> = ObservableField()
init {
loginObservableField.set(LoginModel("Breeze"))
}
fun getUserName():String {
return loginObservableField.get()?.userName?:""
}
fun setUserName(userName:String) {
loginObservableField.get()?.userName = userName
Log.e("ZXX","${loginObservableField.get()?.userName}")
}
}
八、实际使用-更优雅地封装RecyclerView
1、实现思路,使用BindingAdapter为RecyclerView自定义属性
class RecyclerViewBindingAdapter {
companion object {
@JvmStatic
@BindingAdapter("adpter")
fun setAdapter(view : RecyclerView, adapter : RecyclerView.Adapter<*>) {
view.adapter = adapter
}
@JvmStatic
@BindingAdapter("list")
fun setList(view : RecyclerView, vm : RecyclerViewModel) {
(view.adapter as BookAdapter).setList(vm.bookLiveData.value)
}
}
}
2、使用RecyclerView的布局
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="adapter"
type="com.breeze.jetpackstudy.adapter.BookAdapter" />
<variable
name="vm"
type="com.breeze.jetpackstudy.RecyclerViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{adapter}"
app:list="@{vm}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</RelativeLayout>
</layout>
可以看到,这里adapter,LayoutManager,数据List都使用dataBinding进行设置,在Activity中就不需要再为RecyclerView设置这些以往必须要加的属性了
3、Activity代码
class RecyclerViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityRecyclerviewBinding>(this, R.layout.activity_recyclerview)
val model = ViewModelProviders.of(this).get(RecyclerViewModel::class.java)
binding.apply {
//这个lifecycleOwner用到了ViewModel则必须设置
lifecycleOwner = this@RecyclerViewActivity
vm = model
adapter = BookAdapter()
}
}
}
4、列表每一项的布局文件,也是用DataBinding绑定数据
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="book"
type="com.breeze.jetpackstudy.model.Book" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp">
<!--绑定数据-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.name}" />
</RelativeLayout>
</layout>
5、Adapter编写
class BookAdapter : RecyclerView.Adapter<BookAdapter.BookViewHolder>() {
private var list : List<Book>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<BookItemBinding>(inflater, R.layout.book_item, parent,false)
return BookViewHolder(binding)
}
override fun getItemCount(): Int {
return list?.size?:0
}
override fun onBindViewHolder(holder: BookViewHolder, position: Int) {
val book = list!![position]
//刷新数据使用databinding实现,不用再写各种findViewById设置值了~
holder.binding.book = book
}
fun setList(list : List<Book>?) {
if (list != null) {
this.list = list
notifyDataSetChanged()
}
}
class BookViewHolder constructor(val binding : BookItemBinding) : RecyclerView.ViewHolder(binding.root)
}
九、DataBinding实现原理
简单地以MainActivity中的案例分析,只分析单向绑定
看最简单的binding过程,layout的数据如何绑定上的。使用jadx反编译工具查看demo代码,MainActivity代码:
public final class MainActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
这里生成binding
ActivityMainBinding binding = (ActivityMainBinding) DataBindingUtil.setContentView(this, R.layout.activity_main);
Book book = new Book("DataBinding Study", "Breeze", 5);
Intrinsics.checkExpressionValueIsNotNull(binding, "binding");
//这里设置数据后,刷新界面
binding.setBook(book);
new Handler().postDelayed(new MainActivity$onCreate$1(binding, book), 1000);
}
}
ActivityMainBinding的生成过程:
DataBindingUtil.setContentView源码:
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId, DataBindingComponent bindingComponent) {
//这里已经调用了setContentView
activity.setContentView(layoutId);
//这里生成DataBinding对象,
return bindToAddedViews(bindingComponent, (ViewGroup) activity.getWindow().getDecorView().findViewById(16908290), 0, layoutId);
}
bindToAddedView代码会走到bind方法
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root, int layoutId) {
return sMapper.getDataBinder(bindingComponent, root, layoutId);
}
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
//这里是根据layoutId获取序号的逻辑,布局被依次排序为1,2,3...
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if (localizedLayoutId <= 0) {
return null;
}
//DataDinding处理布局会给布局加上tag
Object tag = view.getTag();
if (tag != null) {
switch (localizedLayoutId) {
//这里省略了很多case,方便查看
...
case 3:
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
...
default:
return null;
}
} else {
throw new RuntimeException("view must have a tag");
}
}
关键点来了,ActivityMainBindingImpl中的代码实现的逻辑
//生成的类是继承ActivityMainBinding类的
class ActivityMainBindingImpl extends ActivityMainBinding
//ActivityMainBinding又是继承ViewDataBinding类的
abstract class ActivityMainBinding extends ViewDataBinding
private ActivityMainBindingImpl(DataBindingComponent bindingComponent, View root, Object[] bindings) {
super(bindingComponent, root, 0, bindings[2], bindings[1], bindings[3]);
this.mDirtyFlags = -1;
this.author.setTag((Object) null);
ConstraintLayout constraintLayout = bindings[0];
this.mboundView0 = constraintLayout;
constraintLayout.setTag((Object) null);
this.name.setTag((Object) null);
this.rate.setTag((Object) null);
setRootTag(root);
//这里真正开始处理界面数据
invalidateAll();
}
//真正执行绑定数据的方法,下面会分析如何执行的。
public void executeBindings() {
long dirtyFlags;
synchronized (this) {
dirtyFlags = this.mDirtyFlags;
this.mDirtyFlags = 0;
}
String bookName = null;
int bookRate = 0;
Book book = this.mBook;
String bookRatingUtilGetRatingStringBookRate = null;
String bookAuthor = null;
if ((dirtyFlags & 3) != 0) {
if (book != null) {
bookName = book.getName();
bookRate = book.getRate();
bookAuthor = book.getAuthor();
}
bookRatingUtilGetRatingStringBookRate = BookRatingUtil.getRatingString(bookRate);
}
if ((3 & dirtyFlags) != 0) {
//揭秘了,其实最终就是用的各种BindingAdapter给view设置数据
TextViewBindingAdapter.setText(this.author, bookAuthor);
TextViewBindingAdapter.setText(this.name, bookName);
TextViewBindingAdapter.setText(this.rate, bookRatingUtilGetRatingStringBookRate);
}
}
public void invalidateAll() {
synchronized (this) {
this.mDirtyFlags = 2;
}
//请求绑定,实现在父类的父类ViewDataBinding类中
requestRebind();
}
ViewDataBinding类中requestRebind方法内的逻辑
//这里只列举重要逻辑
private static final boolean USE_CHOREOGRAPHER = SDK_INT >= 16;
//判断了版本大于16,就采用Choreographer在收到vsync信号时执行回调,mFrameCallBack实际只是包装了一下RebindRunnable
if (USE_CHOREOGRAPHER) {
mChoreographer.postFrameCallback(mFrameCallback);
} else {
mUIThreadHandler.post(mRebindRunnable);
}
//RebindRunnable中的代码
public void executePendingBindings() {
if (mContainingBinding == null) {
executeBindingsInternal();
} else {
mContainingBinding.executePendingBindings();
}
}
//最终executeBindingsInternal执行的是ActivityMainBindingImpl类中的executeBindings方法,于是绑定到了数据。在初始化时ActivityMainDataBinding时。
下面分析setBook时如何刷新数据的:
public void setBook(Book Book) {
this.mBook = Book;
synchronized (this) {
this.mDirtyFlags |= 1;
}
notifyPropertyChanged(2);
//可以看到这里实际上也是走到requestRebind方法
super.requestRebind();
}
总结,单相绑定其实最终执行的就是调用各种BindingAdapter类绑定数据,实现界面刷新。而界面刷新的时机是设置数据后,vsync信号来临时(版本大于16时,但现在16以下应该没了吧)