Android Jetpack组件系列文章:
Android Jetpack组件(一)LifeCycle
Android Jetpack组件(二)Navigation
Android Jetpack组件(三)ViewModel
Android Jetpack组件(四)LiveData
Android Jetpack组件(五)Room
Android JetPack组件(六)DataBinding
Android Jetpack组件(七)Paging
Android Jetpack组件(八)WorkManager
Android Jetpack组件(九)DataStore
Android Jetpack组件(十)App Startup
Android Jetpack组件(十一)Compose
首语
Android 布局文件通常只负责UI的布局工作,页面通过setContentView()
关联布局文件,再通过UI控件的id找到控件,接着在页面中通过代码对控件进行操作,因此,页面承担了很大的工作量.为了减轻页面的工作量,Google推出了DataBinding,使得页面和布局之间的耦合度降低。
优势
- 项目更加简介,代码可读性更高。
- 不再需要
findViewById()
。 - 布局文件可以包含简单的业务逻辑。
DataBinding是我第一个使用的Jetpack的组件,用起来是真的舒服。之前为了繁杂的findViewById()
,一直使用ButterKnife(参考之前文章)来代替这些工作。现在官方已经不推荐使用它了,且停止维护。因此,使用DataBinding来代替它。
简单配置
要想使用DataBinding,首先需要在app.gradle中启用它。
android {
.....
dataBinding{
enabled=true
}
}
接着修改布局文件,需要在布局外层添加<layout></layout>
标签,将鼠标移动至布局文件根目录的位置,使用快捷键(alt+enter),选择“Convert to data binding layout”选项,就会自动生成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="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:textAlignment="center"
android:textSize="20sp"
android:text="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
使用
经过简单配置以后,就可以实例化布局文件了,我们删除掉传统的setContentView()
,通过 DataBindingUtil.setContentView()
来实例化文件。实例化返回的布局文件对象,名字和布局文字名字一致,遵循大驼峰命名规则,后面加上Binding。然后通过binding对象得到控件,控件命名遵循小驼峰规则。
ActivityMainBinding binding=DataBindingUtil.setContentView(this,R.layout.activity_main);
binding.textHome.setText("hello databinding!");
数据绑定
如何将数据传递到布局文件中呢?首先,在布局文件中定义布局变量<variable/>
,指定对象的名字和类型,当然数据的操作在<data></data>
标签里。data标签里用于放在布局文件中各个UI控件所需要的数据,这些数据类型可以是自定义类型,也可以是基本类型。
<data>
<variable
name="book"
type="com.yhj.jetpackstudy.Book" />
<variable
name="number"
type="Integer" />
</data>
public class Book {
private int id;
private String title;
private String author;
}
有时我们需要在布局文件中引入一些Java工具类或静态类,处理一些简单的逻辑在布局中,我们可以使用<import />
标签导入。使用alias
,当类名有冲突时,其中一个类可使用别名重命名。默认导入java.lang.*
;
<data>
<import type="com.yhj.jetpackstudy.ui.home.Constants"alias="reName"/>
</data>
布局中的数据绑定使用“@{}”语法写入属性中,通过布局表达式的形式设置TextView
的text。
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{book.title}" />
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{Constants.APP_ID}" />
DataBinding为了方便使用,对布局变量提供了Setter
类,因此,在Activity
中,通过setBook()
,将Book对象传递给布局变量。
Book book = new Book(0, "android", "yhj");
//BR类似于Android中的R类,由DataBinding自动生成,用于存放所有布局变量的id。
//DataBinding为了方便使用提供了Setter类,直接使用setXxx()
//binding.setVariable(BR.book,book);
binding.setBook(book);
绑定后,就不需要再Activity中设置内容了,实现了布局与页面的解耦。
DataBinding具有Null校验,如果绑定值为null,则分配默认值null,如果类型为int
,默认值为0。
表达式语言
在布局中可以包含简单的数据逻辑,可以使用以下运算符和关键字。
- 算术运算符 + - / * %
- 字符串连接运算符 +
- 逻辑运算符 && ||
- 二元运算符 & | ^
- 一元运算符 + - ! ~
- 移位运算符 >> >>> <<
- 比较运算符 == > < >= <=(请注意,< 需要转义为 <)
- instanceof
- 分组运算符 ()
- 字面量运算符 - 字符、字符串、数字、null
- 类型转换
- 方法调用
- 字段访问
- 数组访问 []
- 三元运算符 ?:
- Null 合并运算符
- 视图引用
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
<!--Null 合并运算符-->
android:text="@{user.displayName ?? user.lastName}"
<!--集合-->
android:text="@{list[index]}"
<!--字符串字面量,两种均可-->
android:text="@{map[`firstName`]}"
android:text='@{map["firstName"]}'
<!--资源-->
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
<!--TextView视图引用同一布局中的EditText视图-->
<EditText
android:id="@+id/example_text"
android:layout_height="wrap_content"
android:layout_width="match_parent"/>
<TextView
android:id="@+id/example_output"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{exampleText.text}"/>
事件响应
DataBinding在布局文件中除了绑定数据外,还能够响应用户事件。
首先创建一个事件类,用于接收和响应onClick()
事件。
public class HandleListener {
private Context context;
public HandleListener(Context context) {
this.context = context;
}
public void onClicked(View view) {
Toast.makeText(context, "the button was clicked!", Toast.LENGTH_SHORT).show();
}
}
在<data>
标签中定义布局变量。通过布局表达式,调用onClicked()
.
<variable
name="handler"
type="com.yhj.jetpackstudy.ui.home.HandleListener" />
android:onClick="@{handler::onClicked}"
最后在Activity
中将Activity
与布局绑定,实例化HandleListener
类,传入布局文件。
binding.setHandler(new HandleListener(this));
二级页面的绑定
对于布局层次结构复杂的页面,我们会将部分布局独立成一个单独的布局文件,通过<include />
标签去引用单独的布局文件,也被称为二级页面。
我们在一级页面中绑定数据后,如何将数据传递到二级页面呢?
<!--自定义的命名空间-->
<!--xmlns:bind="http://schemas.android.com/apk/res-auto"-->
<!--bind:book="@{book}"-->
<data>
<variable
name="book"
type="com.yhj.jetpackstudy.Book" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.home.HomeFragment">
<include
app:book="@{book}"
android:id="@+id/include"
layout="@layout/fragment_dashboard"/>
</androidx.constraintlayout.widget.ConstraintLayout>
命名空间app
名字可以自定义,布局变量book也是命名空间xmlns:app
的一个属性。一级页面正是通过命名空间xmlns:app
引用布局变量book,将数据传递给二级页面的。
需要注意的是,数据绑定不支持include
作为merge
元素的直接子布局。merge
是用来帮助在视图树中减少重复布局的。
在二级页面中,我们需要定义一个和一级页面相同的布局变量,用于接收传递过来的数据。然后就可以使用book进行数据绑定了。
<data>
<variable
name="book"
type="com.yhj.jetpackstudy.Book" />
</data>
<TextView
android:id="@+id/text_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{book.title}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
BindingAdapter的原理
DataBinding为我们生成数据绑定需要的各种类,其中包含了大量的静态方法,这些静态方法都有@BindingAdapter
注解,在注解中的别名对应UI控件在布局文件中的属性。
public class ViewBindingAdapter {
....
@BindingAdapter({"android:padding"})
public static void setPadding(View view, float paddingFloat) {
final int padding = pixelsToDimensionPixelSize(paddingFloat);
view.setPadding(padding, padding, padding, padding);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
@BindingMethods({
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tint", method = "setImageTintList"),
@BindingMethod(type = android.widget.ImageView.class, attribute = "android:tintMode", method = "setImageTintMode"),
})
public class ImageViewBindingAdapter {
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, String imageUri) {
if (imageUri == null) {
view.setImageURI(null);
} else {
view.setImageURI(Uri.parse(imageUri));
}
}
@BindingAdapter("android:src")
public static void setImageUri(ImageView view, Uri imageUri) {
view.setImageURI(imageUri);
}
@BindingAdapter("android:src")
public static void setImageDrawable(ImageView view, Drawable drawable) {
view.setImageDrawable(drawable);
}
}
DataBinding以静态方法的形式为UI控件各个属性绑定了相应的代码逻辑,如果在UI控件中的属性使用了布局表达式,那么当布局文件渲染时,绑定它的静态方法自动被调用。
自定义BindingAdapter
在项目开发中,经常使用ImageView
来加载网络图片,但是在布局文件中不能设置图片url,我们可以使用BindingAdapter来解决这个问题。
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* @param view 本身
* @param imageUrl 图片地址
*/
@BindingAdapter(value = {"image_url"})
public static void setImageUrl(CustomImageView view, String imageUrl) {
//网络图片加载框架选择Glide
Glide.with(view).load(imageUrl).into(view);
}
}
通过自定义ImageView
的方式添加静态方法,并给静态方法添加@BindingAdapter
的注解,设置别名为image_url
,布局文件通过别名来调用该方法。
<?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="imageUrl"
type="String" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.yhj.jetpackstudy.ui.home.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image_url="@{imageUrl}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
最后给布局变量赋值。
//Fragment中通过 inflater.inflate,相应的DataBinding通过DataBindingUtil.inflate
//View root = inflater.inflate(R.layout.fragment_notifications, container, false);
FragmentNotificationsBinding binding= DataBindingUtil.inflate(inflater,R.layout.fragment_notifications,null,false);
binding.setImageUrl("https://www.yanghujun.top/web_image/0d9e080e3da409db7d0e0bae3e88bc88.jpg");
多参数重载
在项目开发中除了设置网络图片外,还有如设置图片圆角,圆形图片等需求,通过BindingAdapter都可以实现。
/**
* @param view 本身
* @param imageUrl 图片地址
*/
@BindingAdapter(value = {"image_url"})
public static void setImageUrl(CustomImageView view, String imageUrl) {
setImageUrl(view, imageUrl, false);
}
/**
* @param isCircle 是否圆形图片
*/
@BindingAdapter(value = {"image_url", "isCircle"})
public static void setImageUrl(CustomImageView view, String imageUrl, boolean isCircle) {
setImageUrl(view, imageUrl, isCircle, 0);
}
/**
* @param radius 设置图片圆角
*/
@BindingAdapter(value = {"image_url", "isCircle", "radius"}, requireAll = false)
public static void setImageUrl(CustomImageView view, String imageUrl, boolean isCircle, int radius) {
SecureRandom secureRandom = new SecureRandom();
int i = secureRandom.nextInt(16);
RequestOptions mRequestOptions = null;
RequestBuilder<Drawable> builder = Glide.with(view).load(imageUrl).placeholder(VERTICAL_IMAGES_BG[i]);
if (isCircle) {
mRequestOptions = RequestOptions.circleCropTransform();
} else if (radius > 0) {
//设置图片圆角角度
builder.transform(new RoundedCorners(PixUtils.dp2px(radius)));
}
if (mRequestOptions != null) {
builder.apply(mRequestOptions);
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams != null && layoutParams.width > 0 && layoutParams.height > 0) {
builder.override(layoutParams.width, layoutParams.height);
}
builder.into(view);
}
在@BindingAdapter
注解中,参数以value={"",""}
的形式存在,变量requireAll
设置参数是否必须赋值,默认为true
,同时配合Glide设置图片的圆角、展位图和尺寸等。
最后在布局文件中根据需求来调用静态方法。
<com.yhj.jetpackstudy.ui.home.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:image_url="@{imageUrl}"
app:isCircle="@{true}"
app:radius="@{10}"/>
双向绑定
之前都是使用单向绑定来传递数据,对于一些与用户产生交互的控件,随着字段的变化能更新控件的内容,用户交互时也可以自动得到更新。这就是双向绑定。
使用
项目开发中登录页面必不可少,我们希望用户名字段内容变化时,EditText
自动更新,当用户修改EditText
的内容时,用户名字段同步得到更改。
首先创建一个LoginModel
类,让LoginModel
类的用户名字段和EditText
双向绑定。
public class LoginModel extends BaseObservable {
public String username;
public LoginModel() {
this.username = "yhj";
}
@Bindable
public String getUsername() {
return username;
}
public void setUsername(String username) {
//判断解决循环调用的问题
if (username != null && !username.equals(this.username)) {
this.username = username;
//通知观察者,数据已经更新
notifyPropertyChanged(BR.userName);
}
}
}
在构造器中设置字段初始值,并写了getter()
和setter()
,在getter()
设置@Bindable
注解,告诉编译器,对这个字段进行绑定,setter()
在用户编辑EditText
内容时自动调用。需要进行手动更新。
完成双向绑定只需要将布局表达式中的@{}
变为@={}
即可。username
字段会随着EditText
内容的变化而变化。
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.username}" />
优化
上面的做法有一些弊端,我们的类必须继承BaseObservable
,getter()
添加@Bindable
注解,setter()
还需要手动刷新。
DataBinding提供了ObservableField<T>
,它能将普通对象包装成一个可观察对象。 ObservableField
可以包装各种基本类型、集合数组类型及自定义类型数据。将代码修改可以实现同样的效果。
public class LoginModel {
public final ObservableField<String> username=new ObservableField<>();
public LoginModel() {
this.username.set("yhj");
}
}
需要注意的是,此类的字段应声明为final
,因为绑定仅检测字段值的变化,而不检测字段本身的变化。此类是可拆分和可序列化的,但是在对对象进行拆分/序列化时,将忽略回调,具体说明可参考源码。
其实,DatBinding将基本类型、集合数组、自定义类型进行了封装,提供了诸如ObservableInt
、ObservableDouble
、ObservableArrayList
及ObservableParcelable
等特定的可观察类。
public final ObservableInt age = new ObservableInt();
public ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
public ObservableArrayList<Object> list = new ObservableArrayList<>();
与LiveData和ViewModel
ObservableField
和LiveData作用相似,两者可替换使用,区别在于LiveData与生命周期相关,通常在ViewModel中使用,需要通过observe()
对变化监听。
和ViewModel使用时,可以把对控件的赋值、状态等在布局中进行处理,耦合度更低。
notificationsViewModel = new ViewModelProvider(this).get(NotificationsViewModel.class);
notificationsViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
//binding.textview.setText("yanghujun");
binding.setModel(notificationsViewModel);
}
});
public class NotificationsViewModel extends ViewModel {
public MutableLiveData<String> mText = new MutableLiveData<>();
public NotificationsViewModel() {
mText.setValue("yhj");
}
public LiveData<String> getText() {
return mText;
}
}