目录
前言
DataBinding其实并不是一个新东西,15年 Google IO 大会就开始推了,一线大厂在比较早就开始使用了,随着Jetpack架构组件的发展,使用DataBinding来进行我们的项目开发已经是大势所趋
那么什么是 DataBinding 呢,字面就是帮我们实现视图和数据绑定的工具,现在主流的前端框架(React、Vue)都用到了这种思想。它可以帮助我们实现,直接操作数据就可以自动改变视图。而不是像过去那样,首先要findViewById
拿到组件的引用,当数据发生变化后,我们还需要主动调用组件的相关方法(如setText)进行赋值,才能将数据的改变体现到视图中。DataBinding就是帮助我们实现MVVM架构的最基本的技术支持
导入DataBinding
导入DataBinding非常简单,只需要在app下的gradle文件中配置即可(如下所示)
//build.gradle(:app)
android {
....
buildFeatures {
dataBinding = true
}
}
可以这里有的人会好奇,导入Databinding竟然如此简单,也不用添加任何依赖,这是为什么?
Android Studio中是依靠Gradle来管理项目的,在创建一个项目时,从开始创建一直到创建完毕,整个过程是需要执行很多个Gradle task的,这些task有很多是系统预先帮我们定义好的,比如build task,clean task等,DataBinding相关的task也是系统预先帮我们定义好的,但是默认情况下,DataBinding相关的task在task列表中是没有的,因为我们没有开启dataBinding,但是一旦我们通过 buildFeatures{dataBinding= true}的方式开启DataBinding之后,DataBinding相关的task就会出现在task列表中,每当我们执行编译操作时,就会执行这些DataBinding Task, 这些task的作用就是检查并生成相关DataBinding代码
DataBinding基本使用
创建一个基本的xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
使用快捷键option+enter可以快速生成binding layout
自动转化后的格式,以根标记 layout
开头,后跟 data
元素和 view
根元素
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
</layout>
这里再推荐一款插件,可以简化DataBinding的转换操作并支持和ViewModel和与之关联的layout文件的跳转,可以提升开发时的效率,节省时间,感兴趣的自己点下面的链接进一步了解
https://plugins.jetbrains.com/plugin/9271-databinding-support
现在先建立一个简单的数据模型
public class News {
private String title;
private String content;
public News(String title, String content) {
this.title = title;
this.content = content;
}
public String getTitle() {
return title == null ? "" : title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content == null ? "" : content;
}
public void setContent(String content) {
this.content = content;
}
}
我们再写一个简单的布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="com.geekholt.databinding.bean.News" />
<variable
name="news"
type="com.geekholt.databinding.bean.News" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{news.title}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{news.content}" />
</LinearLayout>
</layout>
这里主要有两点需要关注:
-
data
中的news
变量描述了可在此布局中使用的属性 -
布局中的表达式使用“
@{}
”语法写入特性属性中
数据绑定与整体更新
系统会为每个布局文件生成一个绑定类。默认情况下,类名称基于布局文件的名称,它会转换为 Pascal 大小写形式并在末尾添加 Binding 后缀。以上布局文件名为 activity_main.xml
,因此生成的对应类为 ActivityMainBinding
。此类包含从布局属性(例如,news
变量)到布局视图的所有绑定,并且知道如何为绑定表达式指定值
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main);
News news = new News("Jetpack","一起学习DataBinding");
mainBinding.setNews(news);
}
这样就完成了News
数据和activity_main.xml
的绑定,不仅省略了findViewById
的操作,同时也没有针对单个的View
去进行赋值,当数据发生变化的时候,只需要重新调用mainBinding.setNews(news)
就可以让与数据模型相关的控件都进行刷新
当然,我们也可以针对单个控件进行更新,我们通过一个点击事件,触发news.setContent("一起学习LiveData")
,去更改单个属性
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
News news;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
news = new News("Jetpack", "一起学习DataBinding");
mainBinding.setNews(news);
}
public void submit(View view) {
news.setContent("一起学习LiveData");
}
}
这时候,发现页面并没有发生任何变化,这是为什么呢?
使用BaseObservable
更新单个属性
BaseObservable
是一个基类,需要我们的数据 bean 继承这个基类,然后给属性的 get
方法
添加@Bindable
这个注解,然后在属性的 set
方法中添加上 更新某个字段的方法notifyPropertyChanged
public class News extends BaseObservable {
private String title;
private String content;
public News(String title, String content) {
this.title = title;
this.content = content;
}
@Bindable
public String getTitle() {
return title == null ? "" : title;
}
public void setTitle(String title) {
this.title = title;
notifyPropertyChanged(BR.title);
}
@Bindable
public String getContent() {
return content == null ? "" : content;
}
public void setContent(String content) {
this.content = content;
notifyPropertyChanged(BR.content);
}
}
Obserable接口有一个自动添加和移除监听器的机制,但是通知数据更新取决于开发者。为了使开发变得简单,谷歌创建了BaseObserable这个基础类来集成监听器注册机制。通过给getter方法添加Bindable注解,通知setter方法。
使用ObservableField
来更新单个属性
ObservableFields
是一个对属性添加 DataBinding 更新功能的代理类,针对不同的数据类型有不同类型的 ObservableFields
:ObservableBoolean
、 ObservableByte
ObservableChar
、ObservableShort
、ObservableInt
、ObservableLong
、ObservableFloat
、ObservableDouble
、 ObservableParcelable
等。
//News.java
public class News {
public ObservableField<String> title = new ObservableField<>();
public ObservableField<String> content = new ObservableField<>();
public News(String title, String content) {
this.title.set(title);
this.content.set(content);
}
}
//MainActivity.java
public void submit(View view) {
news.content.set("一起学习LiveData");
}
绑定事件与方法
我们也可以在视图中为点击事件绑定方法
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mainBinding.setClick(new ClickProxy());
}
public class ClickProxy {
public void submit(String content) {
//todo something
}
}
}
通过android:onClick="@{()->click.submit()}"
进行方法绑定,且这种方式支持直接在xml中给方法传参,如下所示
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="click"
type="com.geekholt.databinding.MainActivity.ClickProxy" />
<variable
name="content"
type="String" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={content}" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:onClick="@{()->click.submit(content)}" />
</LinearLayout>
</layout>
这里有一个细节提一下:
前面我们进行数据绑定用的都是
@{}
这种形式,这种绑定方式叫做单向数据绑定;而这里我们给EditText绑定数值用到了@={}
,中间多了一个等号,这种绑定方式称之为双向数据绑定,即当数据发送变化时,页面也发生变化,当我们在EditText中输入文字,页面发生变化时,所对应的数据也会跟着变化
绑定适配器
自动查找属性所对应的方法
目前为止,我们都是对系统控件(如TextView、Button、EditText)进行数据绑定,那如果自定义控件我们如何给它设定属性并进行数据绑定呢?
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setImageUrl(String url) {
Glide.with(getContext()).load(url).into(this);
}
}
ImageView
本身并不能直接设置网络url并加载,当我们在xml中app:imageUrl="@{imageUrl}"
时,数据绑定库会自动帮我们在CustomImageView
中查找setImageUrl(string)
方法,并调用执行
<?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>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.geekholt.databinding.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{imageUrl}" />
</LinearLayout>
</layout>
如果我们想在xml中调用方法名不是以set
开头的方法怎么办呢?
@BindingMethods 自定义方法名称
Databinding库给我们提供了@BindingMethods
注解,使用方式如下所示,主要包括type
、attribute
、method
三个部分
@BindingMethods({@BindingMethod(type = AppCompatImageView.class, attribute = "imageChange", method = "imageChange")})
public class CustomImageView extends AppCompatImageView {
public CustomImageView(Context context) {
super(context);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setImageUrl(String url) {
Glide.with(getContext()).load(url).into(this);
}
public void imageChange(String url) {
Toast.makeText(getContext(), "imageChange:" + url, Toast.LENGTH_SHORT).show();
}
}
<?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>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.geekholt.databinding.CustomImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageChange="@{imageUrl}"
app:imageUrl="@{imageUrl}" />
</LinearLayout>
</layout>
这样一来,每当imageView的url发生变化的时候,就会弹出一个Toast
@BindingAdapter 自定义逻辑
接下里要说的部分,将会是本文的重点,也是将DataBinding应用于企业级项目中的一个关键
很多人对DataBinding的第一印象,似乎都是在xml中编写逻辑代码,就像下面这样:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
android:text="@{@string/nameFormat(firstName, lastName)}"
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
android:text="@{map[`firstName`]}"
需要注意的是这些都不代表着最佳实践,只是说明有这样的功能,支持这样的用法。而且它有一个很大的弊端,相信很多刚入门DataBinding的人都遇到过找不到binding文件的错,然后被劝退,原因无外乎就是表达式写错、variable的类路径不对或者没import相应的类(比如View)等等,都与复杂的表达式有关系,而DataBinding报错也不太智能,有时不能准确定位,所以建议不要在xml里进行复杂的数据绑定
那我们如何来给xml “减负” 呢?
这时候就要用到@BindingAdapter
注解
上文中我们编写的setImageUrl
方法,实际上不仅仅可以在自定义View中使用,我们可以通过@BindingAdapter
作用于ImageView
以及ImageView
的所有子类,如下所示:
public class CommonBindingAdapter {
@BindingAdapter(value = {"imageUrl", "placeHolder"})
public static void loadUrl(ImageView view, String url, Drawable placeHolder) {
Glide.with(view.getContext()).load(url).placeholder(placeHolder).into(view);
}
@BindingAdapter(value = {"visible"})
public static void visible(View view, boolean visible) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
如果 ImageView
对象同时使用了 imageUrl
和 placeHolder
,并且 imageUrl
是字符串,placeHolder
是 Drawable
,就会调用适配器。如果你希望在设置了任意属性时调用适配器,则可以将适配器的可选 requireAll
标志设置为 false
注意:数据绑定库在匹配时会忽略自定义命名空间
<?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" />
<variable
name="placeholder"
type="android.graphics.drawable.Drawable" />
<variable
name="visible"
type="Boolean" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
imageUrl="@{imageUrl}"
placeHolder="@{placeholder}"
visible="@{visible}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
看到这里,是不是觉得这个@BindingAdapter
非常的强大呢?将逻辑判断尽可能的抽取到BIndingAdapter
中,我相信你的代码可维护性会提高一个层次
网友评论