2. Data Binding 中的布局

2.1. 编写 data binding 表达式

2.1.1 DataBinding 的布局文件与以前的布局文件有一点不同。它以一个 layout 标签作为根节点,里面包含一个 data 标签与 view 标签。view 标签的内容就是不使用 data binding 时的普通布局文件内容。例子如下:

<?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>

2.1.2 在 data 标签中定义的 user 变量,可以在布局中当作属性来使用,用来写一些和java代码中表达式类似的"databinding表达式"

<variable name="user" type="com.example.User"/>

2.1.3 在布局文件中属性值里使用 “@{}” 的语法,来表示"databinding表达式"。结合2.2,这里 TextView 的文本被设置为 user 中的 firstName 属性。其中user.firstName和 java 中的表达式含义类似。

<TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>

2.2. 数据对象

刚刚在布局文件中我们使用com.example.User类定义了一个 user 变量,现在我们假设 User 类是一个 plain-old Java object(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的,所以上面这个User类型的对象拥有不可改变的数据(immutable)。在应用中,这种写一次之后永不变动数据的对象很常见。

这里也可以使用 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;
   }
}

从 data binding 的角度看,这两个类是等价的。TextView 的android:text属性的表达式@{user.firstName},对于 POJO 对象这个表达式会读取 firstName 字段的值,对于 JavaBeans 对象会调用 getFirstName() 方法。此外,如果 user 中有 firstName() 方法存在,@{user.firstName}表达式也可以表示对firstName() 方法的调用。

2.3. 数据绑定

上面工作完成后,数据绑定工具在编译时会基于布局文件生成一个 Binding 类。默认情况下,这个类的名字是基于布局文件的名字产生的,先把布局文件的名字转换成帕斯卡命名形式,然后在名字后面接上”Binding”。例如,上面的那个布局文件叫 main_activity.xml,所以会生成一个 MainActivityBinding 类。这个类中包含了布局文件中所有的绑定关系,并且会根据绑定表达式给布局文件中的 View 属性赋值(user变量和user表达式,view绑定,view数据绑定,view命令绑定)。编译时产生Binding类主要完成了2个事情,1.解析layout文件,根据data标签定义成员变量;2.解析layout文件,根据"databinding表达式"产生绑定代码。Binding 创建好之后还需要,创建 Binding 类的对象,并和view绑定。

2.3.1 在 Activity inflate 一个布局的时候创建 binding,例子如下:

@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);
}

就这么简单!运行应用,你会发现测试信息已经显示在界面中了。

DataBindingUtil.setContentView(this, R.layout.main_activity);

这句代码主要做了3件事情,1.把布局设置给Activity,填充为view树;2.创建Binding类对象;3.把view保存在Binding类的成员中,绑定view。这种方式只适合用于Activity中。

2.3.2 也可以通过下面这种方式绑定view:

MainActivityBinding binding = MainActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

MainActivityBinding.inflate() 会填充 MainActivityBinding 对应的布局,并创建 MainActivityBinding 对象,把布局和MainActivityBinding对象绑定起来。

2.3.3 如果在 ListView 或者 RecyclerView 的 adapter 中使用 data binding,可以这样写:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
//or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

2.4. 命令绑定

上面演示了数据绑定,下面演示命令绑定,databinding 还允许你编写表达式来处理view分发的事件(比如 onClick)。事件属性名字取决于监听器方法名字,例如View.OnLongClickListener有onLongClick()的方法,因此这个事件的属性是android:onLongClick。

处理事件有两种方法:

  • 方法绑定:在您的表达式中,您可以引用符合监听器方法签名的方法。当表达式的值为方法引用时,Data Binding会创建一个监听器,并封装方法引用和方法所有者对象,然后在目标视图上设置该监听器。如果表达式的值为null,DataBinding 则不会创建侦听器,而是设置一个空侦听器。

  • Lisenter 绑定:如果事件处理表达式中包含lambda表达式。DataBinding 会创建一个监听器,设置给视图。当事件分发时,侦听器才会计算lambda表达式的值。

2.4.1. 方法绑定

事件可以直接绑定到事件处理器的方法上,类似于android:onClick可以分配一个 Activity 中的方法。与View#onClick属性相比,方法绑定的主要优点是 DataBinding 表达式在编译时就执行过了,因此如果该方法不存在或其签名不正确,您会收到一个编译时错误。

方法绑定和监听器绑定之间的主要区别是,包裹方法引用的监听器实现是在数据绑定时创建的,监听器绑定是在触发事件时创建的。如果您喜欢在事件发生时执行表达式,则应使用监听器绑定。

如果想要将事件处理直接分配给处理程序,那就使用方法绑定表达式,该表达式值是要调用的方法名称。例如,数据对象有如下方法:

public class MyHandlers {
    public void onClickFriend(View view) { ... }
}

绑定表达式就可以像下面这样为视图分配一个点击监听器:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <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}"
           android:onClick="@{handlers::onClickFriend}"/>
   </LinearLayout>
</layout>

注意,@{handlers::onClickFriend}表达式中onClickFriend的方法签名必须与android:onClick监听器对象中的方法签名完全匹配。

2.4.2. Lisenter 绑定

Lisenter 绑定是在程序运行中事件发生时才绑定表达式。它和方法绑定类似,但 Listener 绑定允许运行时绑定的任意的数据表达式。此功能适用于版本2.0及更高版本的Android Gradle插件。 在方法绑定中,方法的参数必须与事件侦听器的参数匹配。在 Listener 绑定中,只要返回值与 Lisenter 预期的返回值匹配就行(除非它期望void)。

2.4.2.1 例如,您有一个 presenter 类,它具有以下方法:

public class Presenter {
    public void onSaveClick(Task task){}
}

然后,通过lambda表达式您可以将点击事件绑定到您的类中,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
      <variable name="task" type="com.android.example.Task" />
      <variable name="presenter" type="com.android.example.Presenter" />
  </data>
  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
      <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="@{() -> presenter.onSaveClick(task)}" />
  </LinearLayout>
</layout>

侦听器只允许 DataBinding 表达式的根元素是lambda表达式。当在表达式中使用回调时,数据绑定自动创建必要的侦听器并且为事件注册。当视图触发事件时,数据绑定才执行给定的表达式。与在正则绑定表达式中一样,在执行这些侦听器表达式时,DataBinding 已经做好了空值和线程安全性的处理。

2.4.2.2 在上面的示例中,我们没有在lambda表达式中定义传递给 onClick(android.view.View)方法的视图参数。侦听器绑定为侦听器参数提供两个选择:1.忽略方法的所有参数;2.命名所有参数。

如果您喜欢命名参数,可以在表达式中使用它们。例如,上面的表达式可以写成:

android:onClick="@{(view) -> presenter.onSaveClick(task)}"

如果你想要使用表达式中的参数,可以在这样使用:

public class Presenter {
    public void onSaveClick(View view, Task task){}
}
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

您也可以使用具有多个参数的lambda表达式:

public class Presenter {
    public void onCompletedChanged(Task task, boolean completed){}
}
<CheckBox
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />

如果正在侦听的事件返回类型不是void类型的值,表达式也必须返回相同类型的值。例如,如果你想监听长点击事件,你的表达式应该返回布尔值。

public class Presenter {
    public boolean onLongClick(View view, Task task){}
}
android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"

如果由于空对象而无法计算表达式,数据绑定将返回该类型的默认Java值。例如,引用类型为null,int为0,boolean为false等。

2.4.2.3 如果需要使用带谓词(例如三元)的表达式,则可以使用void作为符号。

android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

总结起来方法绑定和Listener绑定的区别如下:

  • 方法引用绑定不能是表达式,Lisenter 绑定可以是表达式;
  • 方法引用绑定在绑定的时候会执行 DataBinding 表达式,可以自动处理空指针问题,Listener 绑定在事件触发的时候才会执行 lambda 表达式;
  • 方法引用绑定会限制绑定的方法参数列表,返回值必须和监听器中的方法一致,Listener 绑定只限制 lambda 表达式中语句的返回值和监听器中的方法一致;
  • 方法引用不止能在 android:onClick= 这种命令绑定属性中使用,在其他数据绑定属性中也可以使用,而且可以使用表达式作为参数。

    <TextView
      android:id="@+id/context_demo"
      android:text="@{user.load(context, @id/context_demo)}" />
    
    public String load(Context context, int field) {
      return context.getResources().getString(R.string.app_name);
    }
    

    事件处理除了上述两种方法,还可以直接以数据绑定的形式绑定一个监听器对象(属性setter小节讲解)。

2.4.3. 避免复杂监听器

Listener表达式非常强大,可以使代码非常容易阅读。另一方面,包含复杂表达式的 Listener 又会使布局难以阅读和难以维护。这些表达式应该保持简单,比如只用来从UI传递可用数据到回调方法一样简单,任何业务逻辑还是应该在从侦听器表达式调用的回调方法中实现。

为了避免冲突,有些点击事件处理程序他们需要一个专门的属性,它们不是android:onClick。databinding已通过@BindingMethods注解(属性setter小节讲解)创建以下属性来避免此类冲突。

Class Listener Setter Attribute
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

results matching ""

    No results matching ""