Data Binding Library
https://github.com/soul-ferry/DataBindingDemo
简介:
Data Binding Library
是一个灵活、兼容的support library
,可以在Android2.1(API 7)以上使用。
使用DataBing,Gradle的Android Plugin需要在1.5.0-alpha1以上。
Build Environment
首先需要在Android SDK manager中下载DataBing的library。然后在build.gradle中添加dataBindingg节点,如下:
android {
....
dataBinding {
enabled = true
}
}
如果你的应用中有其他模块或依赖的库用到了Data Binding,也需要在buidl.gradle中配置。
Data Binding 的布局文件
第一个DataBinding表达式
使用DataBinding
后的布局文件与原来的布局文件差距很小,使用DataBinding
后的布局文件的根布局是layout
,并添加了data
节点。布局格式如下
eg.
data
节点中的user
变量描述了可能会在布局中用到的属性值。
DataBinding表达式写在"@{}"
中。在下面的例子中,TextView的text被设置为user
的firstName
Data对象 (ViewModel)
假设你有一个User的POJO
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
这种对象的数据永不会改变。常用于应用中只读一次永之后不改变的数据。
也可以使用JavaBeans:
public class User {
private final String firstName;
private final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
}
在DataBinding
中上面两个User对象是一样的。@{user.firstName}
会访问第一个类的firstName字段第二个类的
getFirstName()
方法。如果存在firstName()
方法,也可能会访问firstName()
方法。
绑定数据
默认情况下,Binding类会根据布局文件的名字自动生成。如main_activity.xml
的布局文件会生成MainActivityBinding
。一个简单的数据绑定如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainActivityBinding binding = DataBindingUtil.setContentView(this,
R.layout.main_activity);
User user = new User("Test", "User");
binding.setUser(user);
}
Event Handling
Data Binding
提供了两种处理事件的方法。
Method References(方法引用)
:你可以直接引用和listener
方法签名一致的方法。当一个表达式被解析为Method References
,Data Binding
会把该方法包装成一个listener
并设置到对应的View
。如果表达式被解析为null
,Data Binding
会设置成对应的listener
为null
。Listener Bindings
:也可以使用lambda
,Data Binding
总是会创建设置到View
上的listener
,当事件被分发到该View
时lambda
表达式会被解析。
Method References
与可以在android:onClick
属性指定调用的Activity
方法类似,事件可以直接绑定到Handler
的方法上。后者的优势在于实在编译时处理,如果方法不存在或签名不一致会得到一个编译时错误。下面的例子展示了该用法:
public class MyHandlers {
public void onClickFriend(View view) { ... }
}
注意:
Handler
中方法必须与监听器中方法参数及返回值一样。如上面的例子中Handler
的方法为public void onClickFriend(View view)
,android:onClick
需要的监听器为OnClickListener
,其方法为public void onClick(View view)
Listener Bindings
Listener Bindings
只要求Handler
中方法与监听器中的方法返回值一致即可。
public class Presenter {
public void onSaveClick(Task task){}
}
在上面的例子中问我们并没有定义传递给 onClick(android.view.View)方法的view参数。Listener bindings
提供了两种传递监听器参数的选择:你可以忽略所有的参数。如果你更喜欢使用带参数的,上面的例子也可以改为
android:onClick="@{(view) -> presenter.onSaveClick(task)}
或者也可以传递更多参数
public class Presenter {
public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}
避免监听器混淆
Listener
表达式非常强大,可以让你写出更易读的代码。另一方面,复杂的Listener
表达式会让你的布局变得难以读懂、管理。Listener
表达式应该尽量简单,只从UI传递有效数据到Callback的方法中。所有的业务逻辑都应该该在Callback的方法中实现。
存在一些特有的点击事件,它们的点击事件的属性并不是android:onClick
。下面的表格列举了对应的属性:
Class | Listener Setter | Attribute |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
Layout Details(布局详情)
Imports
可以在节点使用
import
语法,与java相同java.lang.*
也是自动导入的。
当有相同类名时,可以使用别名:
Variables
Variables
用来描述布局文件中的属性值。
在编译期间会对variable
进行类型检查,只有实现了Observable
接口或是一个observable collection
的variable
才会被观察,即当该variable
的数据发生变化UI
会更新。
为了适配不同的屏幕你可能会有多个布局文件,Data Binding
会合并这些布局文件中的variable
,所以这些布局文件中的variable
不能有混淆。
Data Binding
会为每个variable
自动生成对应的setter
和getter
方法。variable
在调用setter
前都会有一个默认值:引用类型->null
、int
->0
、布尔值->false
。
Data Binding
会自动生成一个context
的variable
。它来值跟布局的getContext()
。可以被用户的variable
覆盖。
Includes
variable
在使用了
布局中的应用:
name.xml
和contact.xml
布局文件都必须有user variable
不支持include
直接作为merge
的直接子元素,如下面的布局:
表达式语言
共同的功能
下面的表达式和Java中的一样
- Mathematical + - / * %
- String concatenation +
- Logical && ||
- Binary & | ^
- Unary + - ! ~
- Shift >> >>> <<
- Comparison == > < >= <=
- instanceof
- Grouping ()
- Literals - character, String, numeric, null
- Cast
- Method calls
- Field access
- Array access []
- T*ernary operator ?:
eg.
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
消失的操作
在Java中可用,但在Databinding
中不可用的操作
- this
- super
- new
- Explicit generic invocation
Null Coalescing Operator(非空运算)
android:text="@{user.displayName ?? user.lastName}"
等于
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
Property Reference(属性引用)
无论Data采用的是那种定义方法,POJO、Java Beans或ObservableFields,在DataBinding
的语法中引用都用下面
的方式。
android:text="@{user.lastName}"
Avoiding NullPointerException(自动避免空指针异常)
Data Binding
生成的代码会自动检查null
值,避免空指针异常。
例如@{user.name}
,如果user
为空,user,name
会被赋值为默认值(null
)。如果是int
类型的默认值为0。
Collections (集合)
常见的集合:arrays, lists, sparse lists, and maps可以通过[]
访问。
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
String Literals (字符串)
如果需要在@{}
表达式内使用""
,布局的属性值需要使用''
android:text='@{map["firstName"]}'
或者使用下面的方式
android:text="@{map[`firstName`}"
android:text="@{map['firstName']}"
Resources (资源文件)
可以使用通常的语法在表达式中访问资源文件:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
格式化的字符串和plurals
android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"
一些资源需要使用指定的引用方式
Type | Normal Reference | Expression Reference |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
Data Objects (数据对象)
任何一个POJO都可以用于Data Binding
,但是修改了一个并POJO不会更新UI。Data Binding
的real power是让你的数据对象(Data Objects
)发生数据改变时能够发出通知。这里有3种数据改变通知机制:Observable objects
, observable fields
, observable collections
。
当一个实现了通知机制的数据对象的数据发生改变并且绑定到了UI,UI会自动更新。
Observable Objects (Observable对象)
Observable
interface有一个添加和移除listeners的机制,但是发送通知取决于开发者。为了方便开发,Data Binding
提供了一个BaseObservable
类,BaseObservable
实现了listener注册机制。实现类要负责在数据改变时发出通知,这需要通过在getter
方法加上@Bindable
注解并且在setter
方法中发出通知。
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}
@Bindable
注解会在编译期间自动在BR
类中生成一个字段,BR
与R
类似。
ObservableFields
Data Binding
还提供了另外更细粒度、更方便的创建Observable
类的方式:ObservableFields
,ObservableBoolean
, ObservableByte
, ObservableChar
, ObservableShort
, ObservableInt
, ObservableLong
, ObservableFloat
, ObservableDouble
, and ObservableParcelable
。
eg.
private static class User {
public final ObservableField firstName =
new ObservableField<>();
public final ObservableField lastName =
new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
如何访问:
user.firstName.set("Google");
int age = user.age.get();
Observable Collections
一些App使用集合保存数据。Observable Collections
允许使用通过键
来访问数据。
ObservableArrayMap
用于键
是引用类型
,如String
ObservableArrayMap user = new ObservableArrayMap<>();
user.put("firstName", "Google");
user.put("lastName", "Inc.");
user.put("age", 17);
在布局文件中的使用
…
ObservableArrayList
键
是int
ObservableArrayList
在布局文件中的使用
…
Generated Binding(生成Binding)
上文已经说过生成的Binding
类名由布局文件名决定。
Creating(创建)
Binding
类应该尽可能早地创建,绑定layout的方式有多种,都是通过调用Binding
类的静态方法。如:
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
Views With IDs(带id的View)
Data Binding
会为布局文件中每个带ID的View生成一个public final
的字段。这可以让替代findViewById
以更快的方式访问View:
生成的Binding类中会有:
public final TextView firstName;
public final TextView lastName;
ViewStubs
布局文件中的ViewStub
经过binding之后会转换成ViewStubProxy
。
简单用代码说明一下,xml 文件与之前的代码一样,根节点改为 layout,在 LinearLayout 中添加一个 ViewStub,添加 ID。
在 Java 代码中获取 binding 实例,为 ViewStubProy 注册 ViewStub.OnInflateListener 事件:
binding.viewStubTest.setOnInflateListener(new ViewStub.OnInflateListener() {
@Override
public void onInflate(ViewStub stub, View inflated) {
StubBinding stubBinding = DataBindingUtil.bind(inflated);
ViewStubViewModel model = new ViewStubViewModel();
model.setName("boco");
stubBinding.setStubModel(model);
}
});
binding.viewStubTest.getViewStub().inflate();
Advanced Binding(高级绑定)
Dynamic Variables(动态设置Variables)
有时候,我们并不知道具体的Binding类型,如在RecyclerView.Adapter
中可能会有多个layout
,我们必须在onBindViewHolder(VH, int)
进行赋值。
public static class BindingHolder extends RecyclerView.ViewHolder {
private ViewDataBinding binding;
public BindingHolder(View itemView) {
super(itemView);
}
public ViewDataBinding getBinding() {
return binding;
}
public void setBinding(ViewDataBinding binding) {
this.binding = binding;
}
}
@Override
public BindingHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
ViewDataBinding binding = DataBindingUtil.inflate(
LayoutInflater.from(viewGroup.getContext()),
R.layout.list_item,
viewGroup,
false);
BindingHolder holder = new BindingHolder(binding.getRoot());
holder.setBinding(binding);
return holder;
}
@Override
public void onBindViewHolder(BindingHolder holder, int position) {
User user = users.get(position);
holder.getBinding().setVariable(BR.user, user);
holder.getBinding().executePendingBindings();
}
Immediate Binding(立即绑定)
当一个variable或者observable发生了改变,数据绑定会在下一帧之前发生但不是立刻发生,如果你希望数据绑定立刻发生需要使用executePendingBindings()
方法,eg.
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
Background Thread
你可以在后台线程中改变不是集合的数据,Data binding will localize each variable / field while evaluating to avoid any concurrency issues.
Attribute Setters (属性赋值)
自动赋值
有了 Data Binding,即使属性没有在 declare-styleable
中定义,我们也可以通过 xml 进行赋值操作。
在上面的例子中app:drawerListener="@{fragment.drawerListener}
会调用DrawerLayout的setDrawerListener(DrawerListener listener)
方法具体的方法参数类型和@{}
中的参数类型一致。
重命名
在上面的xml对应的ViewModel中加入如下注解,app:myText="@{login.name}
便会调用Button的setText方法。
@BindingMethods({
@BindingMethod(type = android.widget.Button.class,
attribute = "myText",
method = "setText"),
})
自定义
ViewModel中的方法
@BindingAdapter({"app:myText"})
public static void showText(Button button,String newV,String oldV){
button.setText(newV);
Log.w(TAG,"new="+newV+"--oldV="+oldV);
}
or
@BindingAdapter({"app:myText"})
public static void showText(Button button,String newV){
button.setText(newV);
Log.w(TAG,"new="+newV);
}
对应得xml代码:
转换器 (Converters)
在 xml 中为属性赋值时,如果变量的类型与属性不一致,通过 DataBinding 可以进行转换。
例如,下面代码中如果要为属性 android:background 赋值一个 int 型的 color 变量:
只需要定义一个标记了 @BindingConversion 的静态方法即可(方法的定义位置可以随意):
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}