在这里将会呈现的是我从 Android 官网、实际开发项目的实践及其他人的实践所学习的关于 Data Binding 的知识。 创建这个 repository 之时,我已经在O2O、B2C、股票投资等类型的上线项目或个人项目中使用了 Data Binding,并且在今后的商业项目中继续使用。
Android 的 Data Binding(数据绑定) 在 Google 的 2015 I/O 上推出,目的在于将逻辑代码和 UI 布局代码更好地绑定在一起,减少 glue code,例如消灭 findViewById()
,自动刷新数据等。Data Binding 支持 API 7+。在 2016 I/O 上 android 官方宣布支持双向绑定,因此可以也利用 Data Binding 在 android 项目中实现 MVVM 架构。
- 保证 xml 内的代码始终在 UI 线程执行,不必担心线程切换的问题。
- 减少在业务逻辑中与 View 的交互,例如 setText(), setImageResource(), etc...
- 性能佳,因为 Data Binding 的一切都发生在编译时,零反射。
- 因为是在编译时产生代码,所以会适当增加编译时间。
- IDE 的智能提示有限,比如在自定义的 attribute 里面目前无法提示。
- 增加调试难度,一个地方写错代码将导致其他 layout 的 binding 出错,出错信息比较隐晦,不过随着版本更新,这个问题在逐步解决。
- Android Plugin for Gradle 的版本需为
1.5.0-alpha1
或以上,以及相应版本的 Android Studio。 - 在 Application module 的
build.gradle
文件加入一下代码:
android { dataBinding { enabled = true } }
- 可以开始写代码了。
- 将
<layout> </layout>
包裹在布局文件中的根布局外。 - 在 gradle plugin 2.2 版本之前需要 Make 或 Build 项目才能使用了,不过在 2.2 之后写了
<layout></layout>
即可使用。
- 在 Activity 中初始化:
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DataBindingUtil.setContentView(layoutId, this);
}
这就相当于 setContextView() ,这个方法返回的是相应的 xml 文件或动态生成的 java 文件,里面包含了所有 view 的实例,可供直接使用而不再需要 findViewById(), 这里说的 view 实例指的是在 xml 文件里面写了 id 的 view,不写 id 是不会拿到 view 实例的.
在 activity 中实例的方法还有:
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View view = LayoutInflater.from(this).inflate(layoutId, null);
ActivityBinding.bind(view);
}
同样的,这个 bind() 方法也是相应的 xml 文件或返回 java 文件。
一般来说,按照以上配置的 binding 是指向相应的 xml 文件,不过以 apt 的方式去编译资源文件就可以得到相应的 java 文件,即在 module 的 build 文件顶部加入apply plugin: 'com.neenbedankt.android-apt'
和在 root 的 build 文件里面的 dependencies 加入 classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
, 这样生成的 binding 文件就是 java 文件。
2. 在 Fragment 中初始化:
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
FragmentBinding binding = FragmentBinding.inflate(inflater,container, false);
return binding.getRoot();
}
同样的,该方法指向相应的 xml 文件或相应的 java 文件,同时,在 fragment 中初始化还有以下方法:
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.from(this).inflate(layoutId, null);
FragmentBinding.bind(view);
return view;
}
Activity 的初始化使用到了 DataBindingUtil 的 setContentView 方法:
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId,
DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
ViewGroup parent, int startChildren, int layoutId) {
final int endChildren = parent.getChildCount();
final int childrenAdded = endChildren - startChildren;
if (childrenAdded == 1) {
final View childView = parent.getChildAt(endChildren - 1);
return bind(component, childView, layoutId);
} else {
final View[] children = new View[childrenAdded];
for (int i = 0; i < childrenAdded; i++) {
children[i] = parent.getChildAt(i + startChildren);
}
return bind(component, children, layoutId);
}
}
当然还有 bind(), infalte(), bindTo() 等方法可以用来绑定布局,不局限在 Acitivty 或 Fragment 中使用,不过在我的实践中发现,Data Binding 还不能很好支持自定义控件的绑定,还是需要使用传统的方式,例如 findViewById, 不过这个情况应该会在以后得到更好的支持,Data Binding 还会带来更多惊喜。
创建一个 User 对象,在 xml 中加入一下示例代码:
<data>
<variable
name="user"
type="me.knox.learningdatabinding.User"
</data>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.gender}"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.age}"
/>
就这样,即使控件没有 id 可以引用,只要往 xml 赋值一个 User,这三个 TextView 就会自动显示设置的内容。
在 Activity 或 Fragment 中赋值。这时,binding 文件已经生成了 setUser() 方法,只要在适当的时候调用这个 setUser() 方法即可,例如 binding.setUser(user)。
在 xml 中,加入如下代码:
<EditText
android:id="@+id/edt_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="type in to see binding works"
/>
这样就可以在绑定的布局实例中找到这个 EditText 控件,一般为驼峰命名,如 edtTitle, 然后在 Activity 或 Fragment 中加入如下代码:
binding.edtTitle.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override public void afterTextChanged(Editable editable) {
String s = editable.toString();
binding.tvTitle.setText(s);
}
});
在 Data Binding 中就是这样使用控件的实例,无需 findViewById()。
这里使用 RecyclerView 来展示一个列表。
首先可以继承 ViewDataBinding
来写一个 ViewHolder 通用父类,更方便在项目中使用,代码如下:
public class DataBoundViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
private T binding;
public DataBoundViewHolder(T binding) {
super(binding.getRoot());
this.binding = binding;
}
public T getBinding() {
return binding;
}
}
然后写一个 item_rv.xml:
<layout>
<data>
<variable
name="user"
type="me.knox.learningdatabinding.User"/>
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{user.name}"
android:gravity="center"
/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{user.gender}"
android:gravity="center"
/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@{user.age}"
android:gravity="center"
/>
</LinearLayout>
</layout>
再写一个 RecyclerViewAdapter:
public class RecyclerViewAdapter extends RecyclerView.Adapter<DataBoundViewHolder<ItemRvBinding>> {
private List<User> users;
public RecyclerViewAdapter(List<User> users) {
this.users = users;
}
@Override
public DataBoundViewHolder<ItemRvBinding> onCreateViewHolder(ViewGroup parent, int viewType) {
ItemRvBinding binding = ItemRvBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new DataBoundViewHolder<>(binding);
}
@Override public void onBindViewHolder(DataBoundViewHolder<ItemRvBinding> holder, int position) {
holder.getBinding().setUser(users.get(position));
holder.getBinding().executePendingBindings();
}
@Override public int getItemCount() {
return users == null ? 0 : users.size();
}
}
然后在 Activity 或 Fragment 中生成的 RecyclerView 的实例传入这个 adapter,用法和没有使用 Data Binding 一样,这就是简单地把数据绑定到 RecyclerView 并显示出来。ListView 使用 Data Binding 也类似以上的写法,Data Binding 只是使 adapter 绑定 item view 更方便。
- 数学运算 + - / * %
- 字符串连接 +
- 逻辑 && ||
- 二元 & | ^
- 一元 + - ! ~
- 移位 >> >>> <<
- 比较== > < >= <=
- instanceof
- Grouping ()
- 字面 character, String, numeric, null
- Cast
- 调用方法
- 访问 field
- 访问数组
- 三元 ?:
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
在 xml 中我们会用到控件的很多 attribute(属性),假如需要在 java 代码中写很多 setter,例如 setText(), setBackgroundColor() 等等,这会增加代码量,不好复用,比如 TextView 的 setText() 方法在调用前需要很多逻辑处理传入的字符串,而这个逻辑处理在两个完全不同的 Activity 中不同的 TextView 的 setText() 方法都用到了,虽然可以把这个逻辑写到一个共用的类中(例如 Utils),但是我觉得这不够优雅。Data Binding 可以把种情况写得相对比较优雅一点,因为 @BindingAdapter
, @BindingConversion
, 有了这两个 annotation 之后将减少可观的代码量,也会使代码更容易维护。
下面举例:
User 这个类的 name 在赋值到一个 TextView 之前需要做一些处理,例如判空或其他复杂逻辑处理,然后又不想写一大块的 setText() 代码,虽然有了 Data Binding 也不可能直接把逻辑写到 xml 里面去,否则会造成 xml 可读性变差,也不好调试问题,这时候可以使用 @BindingAdapter
。
@BindingAdapter("name")
public static void setUserName(TextView tv, String name) {
if(name == null) return;
tv.setText(name);
}
这就会生成一个 namespace 为 name 的 attribute 了,这里包含了一个判空逻辑,这样处理的好处我认为是,如果赋值为空就不需要调用 setText() 方法了,而且如果赋值为空 Data Binding 也会自动避免空指针,赋值 null,要知道 TextView 每次 setText() 都会触发 requestLayout(), 假如是在一个需要频繁处理文本的地方 setText(null),这可能会造成多次绘制从而 overdraw。直接在 TextView 这样使用:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:name="@{user.name}"
/>
如果你想在 xml 里面做字符串格式处理,例如:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:name="@{@string/name(user.name)}"
/>
直接引用字符串资源就行了,遗憾的是,目前并未支持字符串资源智能提示,而且在 xml 里也不能直接跳转目标字符串,修改的话需要手动去 strings.xml 查找。
TextView 是不能直接显示一个 double 类型的值的,虽然可以先在 java 代码里面转换成字符串再赋值,但是如果要复用这个转换呢?而且如果需要对这个 double 值约小数位呢?这时候 @BindingConversion
派上用场了。
@BindingConversion
public static String displayMoney(double money) {
return String.format("%.2f", money);
}
这样, TextView 显示的就是约两位小数的字符串了,直接在 xml 这样写:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.money}"
/>
无需特定的 attribute,直接赋值。