引言
原文地址:https://developer.android.com/topic/libraries/data-binding/index.html#generated_binding
其实我最开始听说的butterknife,databinding根本没听过。。
翻官方文档才知道有这么个东西,那肯定要问了,用它干嘛
好了,这篇已经有人翻译了,然而我看了下还是不全。。
参考:
(译)Data Binding 指南(有部分自己没看明白的,这里翻的很通畅)
https://github.com/LyndonChin/MasteringAndroidDataBinding(配合demo说明,感觉有些简洁)
棉花糖给 Android 带来的 Data Bindings(数据绑定库)(有几处没有详细示例,自己调用失败的,这里参考了一下)
Data Binding 库
绑定layout和code,减少代码量
支持Android2.1以上
需求Android Plugin for Gradle 1.5.0-alpha1 or higher
需求AS 1.3以上
构建环境
module的gradle.build文件下:
android {
....
dataBinding {
enabled = true
}
}
常规用法:bean和layout绑定
Layout配置
比起常规配置,加了个layout和data标签
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/>
</LinearLayout>
</layout>
纵观一下,不难明白,data标签的name对应的值是下面view引用的实例名;type对应的类,是下面view引用的实例类。
数据类
POJO就行,结构如下:
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
注意要final的。你也可以设置成JavaBean的样式,就是域改成private,增加setter\getter。
代码中使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//这个类名根据avtivity对应的layout名,加个binding自动生成
//比如此处 main_activity =》 MainActivityBinding
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
//你也可以这么获取
// binding = MainActivityBinding.inflate(getLayoutInflater());
// setContentView(binding.getRoot());
User user = new User("Test", "User");
binding.setUser(user);
}
但是使用列表的时候你会发现,需要在Adapter中用:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup,
false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemMainBinding binding;
if(convertView == null) {
binding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.item_main, parent, false);
convertView = binding.getRoot();
} else {
binding = (ItemMainBinding) convertView.getTag();
}
binding.setItem(this.getItem(position));
convertView.setTag(binding);
return convertView;
}
处理事件
和基本的绑定(在android:txt中绑定值)类似,而进行事件的绑定时,属性要根据事件的类型进行匹配。比如点击的要在onClick中,长按的在onLongClick。
有如下面两节所示的两种方式:
方法引用:
类似于在onClick属性中写方法名的方式,在编译时就能判断是否正确。
监听的实现是在数据绑定的时候,而不是事件触发的时候。
处理类
public class MyHandlers {
//参数需要是事件监听器的参数,此处为view
public void onClickFriend(View view) { ... }
}
layout
<variable name="handlers" type="com.example.Handlers"/>
...
<View
...
android:onClick="@{handlers::onClickFriend}"/>
Main
然后你会发现并没有反应,因为你需要绑定。。我是参考
//你可以直接把onClickFriend写在mainActivity里,这样setHandlers(this)就好了
//好了问题来了,那这个方法的存在意义是什么,最最原生的onClick不就是这样的吗,还简单很多
MyHandlers handlers = new MyHandlers();
binding.setHandlers(handlers);
###Listener绑定:
监听的实现是在触发的时候。
需求:Android Gradle Plugin for Gradle version 2.0 and later.
####处理类
public class Presenter {
public void onSaveClick(Task task){}
}
layout
<variable name="task" type="com.android.example.Task" />
<variable name="presenter" type="com.android.example.Presenter" />
...
<View
...
<!--lambda形式,view参数被忽略了-->
android:onClick="@{() -> presenter.onSaveClick(task)}"
带上View参数
//此时view参数被带上,但是没有用它
android:onClick="@{(view) -> presenter.onSaveClick(task)}"
或者
public class Presenter {
public void onSaveClick(View view, Task task){}
}
//如果你要用他,肯定是要带上参数名(名字可以被更改)
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
带上其他参数
如果有需要,你可以遵循lambda,设置多个参数:
//这里有on,下面调用的时候没有,我测试的时候用的一样的名字,可行。
public class Presenter {
public void onCompletedChanged(Task task, boolean completed){}
}
//没有onCheckedChanged属性,但是这里就是用这个值
//cb表示的是view参数,isChecked是CheckBox的属性,两个作为参数传递
<CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
根据实际event设置返回值
//比如onLongClick是要返回true或false表示事件消费的,那么presenter里的方法也是boolean返回
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"
public class Presenter {
public boolean onLongClick(View view, Task task){}
}
三元语法判断是否返回:
重看
//这里的void表示不作什么
//话说view没有这个isvisible的属性啊???不过换了个属性也不好使
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
注意事项
尽量保持listener的简洁,因为他的初衷就是使代码易于维护(易读、易修改)。所以对于一些逻辑操作请放在外部调用的地方处理。
避免冲突
类名 | 监听器setter | 属性 |
---|---|---|
SearchView | setOnSearchClickListener(View.OnClickListener) | android:onSearchClick |
ZoomControls | setOnZoomInClickListener(View.OnClickListener) | android:onZoomIn |
ZoomControls | setOnZoomOutClickListener(View.OnClickListener) | android:onZoomOut |
Layout(标签)细则
Imports
在data中导入类,使用方式像java
<data>
<import type="android.view.View"/>
<!--类名冲突,别名-->
<import type="com.example.real.estate.View"
alias="Vista"/>
<import type="com.example.User"/>
<variable name="user" type="User"/>
</data>
...
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"
<!--提示:AS还不能很好的自动处理variable导入,如上面的user
但是我使用的时候没出现问题
user.connection是为了解决这个问题,我尝试了一下反倒有问题。。-->
<TextView
android:text="@{((User)(user.connection)).lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<!--静态方法调用-->
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
Variables
上面的使用之后,variables的大意已经了然于胸了。
- 变量类型会在编译时被检查
所以如果变量声明了 Observable 接口或者是一个observable collection,那它会被反射使用。如果变量是一个没有实现 Observable* 接口的基类或接口,变量的变动不会被观测到(即不会引起 UI 的变化)!
双向支持的内容请查下下文 数据对象
- 不同layout(比如横向、纵向)的变量会合并在一起,如果命名重复会有冲突。
- 定义variable之后,binding会自动生成setter\getter方法。在未被setter之前,变量值的初始化和java类似: 引用类型为null ,int为0 , 布尔为false,etc..
- binding 类也会生一个名为 context 的特殊变量,这个变量可以被用于表达式中。context 变量其实就是 rootView 的 getContext() 的返回值。context 变量会被同名的显式变量覆盖。
自定义Binding类名
如果 module 包名为 com.example.my.app,binding 类默认会被放在 com.example.my.app.databinding 中
<!--重命名-->
<data class="ContactItem">
...
</data>
<!--包前缀改到module包下,即没有databinding,需要“.”前缀-->
.ContactItem
<!--任意包名-->
com.example.ContactItem
Includes
当要包含一个布局的时候,使用bind:user=”@{user}”的形式,会自动添加命名空间。
<include layout="@layout/name"
bind:user="@{user}"/>
注意:
- name.xml中也要声明user variable。。
- merge的直接子布局不能使用
表达式语言
常规元素
查看原文或者参考文章即可。。
缺失操作
this、super、new、明确的泛型调用
Null Coalescing Operator
<!--双问号-->
android:text="@{user.displayName ?? user.lastName}"
<!--等价于-->
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
属性引用
你已经用了千百遍了。(user.name)
杜绝空指针
自动处理空指针,即引用为空则为null, int则为0 , etc…
容器类
<!--列表-->
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List<String>"/>
<variable name="sparse" type="SparseArray<String>"/>
<variable name="map" type="Map<String, String>"/>
<variable name="index" type="int"/>
<variable name="key" type="String"/>
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
字符串
直接量
外围用单引号,内部用双引号就行了
如果外围用双引号,内部用1键边上的 ` (文档说单引号也可以,但是我这不行)
字符串的拼接
字符串拼接:’@{“image_” + id}’
资源引用
重看,plurals
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
<!--字符串-->
<!--可以设置参数,即StringFormat的感觉,在nameFormat中添加对应s%-->
android:text="@{@string/nameFormat(firstName, lastName)}"
<!--plurals,还没用过。。尴尬-->
android:text="@{@plurals/banana(bananaCount)}"
需求类型说明
类 | 常规引用 | 表达式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
数据对象
现在POJO的绑定情况基本已经说完了。但是如果更改了数据,显然UI是不会改变的。
要实现UI的改变,有三个机制:
可观察对象
private static class User extends BaseObservable {
private String firstName;
private String lastName;
//注意到标签,标签在编译时在BR类(类似R文件)里生成一个元素,
@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);
}
}
如果基类不能修改,那么可以自己实现:
重看,没实现
今天重新使用发现,firstName要符合驼峰规则(不能用全大写),getFirstName()的写法也一样,这样编译了之后BR中才找的到。。具体要多符合就不去测试了。
public class Item implements Observable {
private PropertyChangeRegistry callbacks = new …
…
@Override
public void addOnPropertyChangedCallback(
OnPropertyChangedCallback callback) {
callbacks.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(
OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}
}
可观察数据域
private static class User {
//会自动根据类型装箱
public final ObservableField<String> firstName =
new ObservableField<>();
public final ObservableField<String> lastName =
new ObservableField<>();
//直接指明int
public final ObservableInt age = new ObservableInt();
}
重看,没变化
//代码中调用
user.firstName.set("Google");
int age = user.age.get();
可观测容器类
//除了加个Observable,其他和上面不可观测的使用一样。
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
顺便还讲了个技巧,用ObservableArrayList和布局绑定的时候,可以预见,属性的顺序肯定要长久固定,比如age,是第三个属性,list[2],那么2可以用静态常量表示,写成list[Feilds.AGE]。可读性大增
绑定的生成
不管是否自定义,binding类全部继承自ViewDataBinding
创建
最常规的用法
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
有时候layout通过不同机制已经inflater了。可以直接binding
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
有时候binding(类型)不能预先知道
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
这么多款式,总有一款适合你
带有ID的view
会根据layout中view的id(有的话),在binding类中生成一个public final域
这里的传递机制会比很多view的findViewById方式要快。
Variables
//在binding中样式如下,类型自动会匹配
public abstract com.example.User getUser();
public abstract void setUser(com.example.User user);
ViewStubs
重看,没用过
ViewStub有些特殊,它本质上不存在于view结构,binding 类中的类也得移除掉,以便系统回收。因为 binding 类中的 View 都是 final 的,所以我们使用了一个叫 ViewStubProxy 的类来代替 ViewStub。开发者可以使用它来操作 ViewStub,获取 ViewStub inflate 时得到的视图。
当 inflate 一个新的布局时,binding重新确立。因此,ViewStubProxy 必须监听 ViewStub 的 ViewStub.OnInflateListener,并及时建立 binding。由于 ViewStub 只能有一个 OnInflateListener,你可以将你自己的 listener 设置在 ViewStubProxy 上,在 binding 建立之后, listener 就会被触发。
进阶绑定
动态变量
比如在recycler的使用中,item的layout没法预先知道,得在 onBindViewHolder(VH, int).中绑定。
重看,没实现
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = mItems.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}
立即执行
当可观测物改变时,binding都会在下一帧的时候执行变化,不管如何都需要时间。如果要强制马上执行,调用executePendingBindings()方法。
后台线程
只要数据不是容器类,你可以直接在后台线程做数据变动。Data binding 会将变量/域转为局部量,避免同步问题。
明天再看… …