Data Binding Library官网文档翻译 2 -布局和binding表达式

这一篇是整个系列的第二篇翻译,像上一篇一样,翻译不准确的地方欢迎大家批评指正。同样附上官网链接。同时,转载请注明出处,https://www.jianshu.com/p/df795c781e50

1. 前言

你可以使用表达式来处理控件分发过来的事件。Data binding库可以自动生成binding class,来绑定布局中的控件和数据类。

使用了Data binding的布局文件内容会和普通的布局文件稍有不同。主要是我们在需要设置layout作为根标签(root tag),里面包含一个data标签和一个正常的view标签。举例如下:

<?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标签中的user变量可以在layout文件中使用。如图,databinding表达式需要使用"@{}"语法,上面代码中的TextView的text被设置成了user变量的firstName和lastName属性。

注意,Databinding表达式应该保持短小精悍,因为他们无法集成测试而且IDE的支持有限。遇到复杂的表达式,我们可以使用自定义binding适配器来解决custom binding adapters

2. 数据对象

我们现在假设您有一个普通的对象来描述User实体。这种对象的数据永远不会改变(final)。在应用程序中它们只会被读一次之后永远不再改变。注意这里的firstName和lastName都是public的。

public class User {
  public final String firstName;
  public final String lastName;
  public User(String firstName, String lastName) {
      this.firstName = firstName;
      this.lastName = lastName;
  }
}

当然也可以使用遵循一组约定的对象,例如get,set方法,注意这里的firstName和lastName都是private的,如以下示例所示:

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的角度来看,这两个类是等价的。用于android:text属性的表达式@ {user.firstName},在前一类中直接访问firstName字段,而后一类中调用getFirstName()方法,来获取firstName的内容。或者,如果firstName()方法存在也会使用这个方法。

2. Binding data

每个layout文件都会生成一个对应的binding类。默认情况下,这个binding class的文件名是基于layout文件名的。将layout文件名按照Pascal命名规则转换,再添加一个suffix后缀即可。例如activity_main.xml文件的对应binding class的名字就叫ActivityMainBinding。

binding class包含了layout中的属性(比如上面的user变量)以及布局的各种View,并且能为binding表达式赋值。(即通过binding class可以访问到layout中的data和各个view)。我们建议在inflate布局的时候创建bindings。一般Activity中可以这样使用:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
   User user = new User("Test", "User");
   binding.setUser(user);
}

上面的代码在运行的时候会在UI中展示“Test”和“User”。除此之外也可以用下面的方法获取View:

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

如果您在Fragment,ListView或RecyclerView适配器中使用数据绑定项,您可能更喜欢使用binding class(binding类的父类)或DataBindingUtil类的inflate()方法,如以下代码示例所示:

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

3. 表达式语言

3.1 Common features

表达式语言和managed code中的表达式很类似。你可以在表达式语言中使用以下操作符和关键字:

  • Mathematical + - / * %
  • String concatenation +
  • Logical && ||
  • Binary & | ^
  • Unary + - ! ~
  • Shift >> >>> <<
  • Comparison == > < >= <=
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • Method calls
  • Field access
  • Array access []
  • Ternary operator ?:

例如

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

3.2 Missing operations

以下的操作符无法在表达式语言中使用

  • this
  • super
  • new
  • Explicit generic invocation(显式通用调用)

3.3 Null coalescing operator 空结合运算符

A ?? B,这个运算符会在左边不为null的时候使用左边,否则使用右边

android:text="@{user.displayName ?? user.lastName}"
// 这两个表达式效果相同
android:text="@{user.displayName != null ? user.displayName : user.lastName}"

3.4 属性引用

表达式可以通过下面的格式来引用class中的属性,对于fields,getters和ObservableField对象使用方法都是一样的(这一点很重要,可以回去重看 2.数据对象)。

android:text="@{user.lastName}"

3.5 避免空指针错误

生成的数据绑定代码会自动检查空值并避免空指针异常。例如,在表达式@ {user.name}中,如果user为null,则为user.name分配其默认值null。如果引用user.age,其中age的类型为int,则数据绑定使用默认值0。

3.6 集合Collections

为方便起见,可以使用[]运算符访问常见的各种集合,例如数组arrays,列表list,稀疏列表sparse lists和maps。

<data>
    <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"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"

注意,针对map。还可以使用object.key表示法引用map中的value。例如,上面示例中的@ {map [key]}可以替换为@ {map.key}。

3.7 字符串文字

您可以使用单引号括起属性值,然后再表达式中使用双引号,双引号括起来的会被当做是string字符串。如以下示例所示

android:text='@{map["firstName"]}'

当然,用双引号包裹属性值,表达式中使用单引号也是可以的。

android:text="@{map[`firstName`]}"

3.8 资源文件

使用如下语法即可在表达式用引入资源文件

android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

提供相关参数之后也可以使用Format strings and plurals功能。(格式化string我们经常用,可以动态替换string字符串中的某些文字。format plurals主要是针对英文中单复数时文字本身会有变化而生成的功能)

android:text="@{@string/nameFormat(firstName, lastName)}"
android:text="@{@plurals/banana(bananaCount)}"

当复数采用多个参数时,应传递所有参数:


  Have an orange
  Have %d oranges

android:text="@{@plurals/orange(orangeCount, orangeCount)}"

有些资源需要明确指明类型,如下表中:

image

4. 事件处理Event handling

Data binding允许你编写表达式来处理views分发来的事件,比如onClick方法。事件的属性名称往往由listener中的方法名称来确定,但也有一些例外。比如,View.OnClickListener有onClick方法,所以这个时间的对应属性名就叫做 android:onClick。

接下来解释一下上面说到的例外情况。因为存在一些事件处理方法,它们的方法名也是onClick,但是为了避免冲突,就设置了别的属性。其实原因是setXXXlistener时传入了OnClickListener,没有针对自身额外写一个新的Listener。如下:

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

上面说了一些注意事项,现在来具体看一下。我们有如下两种机制来处理事件。

  • Method references(方法引用): 在表达式中,您可以遵循Listener中的方法的签名,来引用这个方法。当表达式为Method references时,Data binding将方法引用和所有者对象(即传入到onClick中的View参数)一起包装在listener中,并在目标视图上设置该侦听器。如果表达式求值为null,则数据绑定不会创建Listener,而改为设置空侦听器(即setOnClickListener)。

  • Listener bindings:事件发生时,会计算我们设置的lambda表达式(即这个方法是将属性设置为一个lambda表达式)。给View设置之后,每次事件被分发过来,Data binding都会创建一个listener,然后计算lambda表达式。

4.1 Method references

事件可以直接绑定到处理程序方法,类似于android:onClick中可以写一个对应的Activity中的定义的方法。就是在Activity中写一个比如onTestClick方法,然后设置android:onClick="onTextClick"。下面举个例子:

// XML里的代码
 <TextView
        android:id="@+id/tv"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="#ffcc00"
        android:clickable="true"//设置可以点击,TextView默认是不能点击的
        android:onClick="showMsg"//设置点击的方法名
        android:text="设置单击事件"
        android:singleLine="true"
        android:tag="hello" />


//  在Activity中设置单击方法,方法名要和XML中的一样
public void showMsg(View view){
   //必须写参数,View是事件源,回调的时候传入事件源
    Toast.makeText(this, tv.getTag().toString(),Toast.LENGTH_LONG).show();
}

与上面代码里的方案相比,Method references主要优点是表达式在编译时处理,因此如果该方法不存在或其签名不正确,则会收到编译时错误。

要将事件分配给他的handler处理者,我们需要正确的使用binding表达式,对应属性里填入要调用的方法名称。比如:

// 处理者,包裹了处理方法
public class MyHandlers {
//  处理方法,注意参数是View
    public void onClickFriend(View view) { ... }
}


// XML代码
// 如下代码,会将view的clickListener设置为onClickFriend方法
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.MyHandlers"/>
       <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>

注意,表达式中调用的方法的签名,要跟对应的listener中的方法的签名相同。比如android:onClick对应OnClickListener的onClick(View view)方法,所以handlers::onClickFriend方法在定义的时候也是传进对应的View参数。

4.2 Listener bindings

Listener bindings(侦听器绑定)是在事件发生时运行的绑定表达式。它们类似于方法引用,但你可以使用更灵活的binding表达式。Android Gradle Plugin for Gradle 2.0 以上版本提供此功能。

使用method references时,method的参数必须与对应的事件监听器的参数保持一致。而在listener bindings中,你只需要保持返回值与事件监听器的预期返回值一致,除非它期望返回值是void。下面给一个例子~

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

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

在表达式中使用回调时,data binding会自动创建必要的Listener并为事件注册它。当View触发事件时,Data binding会计算给定的表达式。与常规绑定表达式一样,在计算这些侦听器表达式时,我们只能拿到空的表达式,并且保持线程安全。

4.2.1 lambda表达式中参数的定义

上面的例子中,我们没有定义传递给onClick方法的view参数。listener bindings为监听器参数提供了两种选择。第一种,可以选择忽略所有参数。第二种,命名所有参数(注意,要么都命名,要么一个都不命名)。 例如,上面的表达式可以写成如下:

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

如果你需要在表达式中调用监听器的参数,那么就需要命名他们。:

public class Presenter {
    // 增加一个View参数
    public void onSaveClick(View view, Task task){}
}

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

如果参数超过一个:

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)}" />
4.2.2 Listener bindings的返回值问题

需要注意的是,如果监听的时间返回值不是void,那么我们定义的表达式就必须返回相同类型的值。比如我们想要监听long click event。我们应该向下面这样写

public class Presenter {
    // 注意这里不是 void 而是 boolean
    public boolean onLongClick(View view, Task task){}
}

android:onLongClick="@{(theView) -> presenter.onLongClick(theView, task)}"


// longclick 注意onLongClick返回值为boolean
setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
            return false;
        }
    });
    
    // longclick 注意onClick返回值为void
setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
                    
        }
    });

注意表达式应该尽量简洁,复杂的逻辑应该放到监听器表达式的回调方法中执行。

4.3 两者的区别

Method references和Listener bindings之间的主要区别在于

  1. 前者是在绑定数据时创建的,而不是在触发事件时创建的。如果您希望在事件发生时执行代码,则应使用后者。

  2. 前者需要方法的签名(即形参和返回值)与对应listener一致。而后者只要求保持返回值一样就可以(如果返回值是void,那么返回值也可以不同)。

个人理解,用代码来说大致是这个意思:

// Method references
android:onClick="@{handlers::onClickFriend}"

// 效果相当于写了一个onClick方法
tv.setOnClickListener(new View.OnClickListener() {
            // onClickFriend与onClick等价
            // 所以使用Method References时两者参数要保持一致
            handler::onClickFriend(v);
    });
// Listener bindings
android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"

// 这样设置相当于新写了一个ClickListener
tv.setOnClickListener(new OnXXXClickListener(){
    // 可以传入onClick中的参数
    public void onXXXClick(View theView, Task task) {
    // task是xml里设置的
    presenter.onSaveClick(theView, task);
    }
});

// lambda公式与下面等价
new OnXXXClickListener(){
    // 可以传入onClick中的参数
    public void onXXXClick(View theView) {
        // task是xml里设置的
        presenter.onSaveClick(theView, task);
        }
    }

4.4 注意事项 - 避免listener过于复杂

监听器表达式非常强大,可以使您的代码非常容易阅读。另一方面,如果Listeners包含复杂表达式,也会使您的布局难以阅读和维护。这些表达式应该像将UI中的可用数据传递给回调方法一样简单。复杂的业务逻辑应该在回调方法内部进行实现,而不是卸载listener 表达式里。


5. Imports, variables, and includes的用法

数据绑定库提供imports, variables, 以及 includes等功能。这些也都是最常用的功能。

import使布局文件中更容易引用别的class。variables可以在绑定表达式中访问类的属性。include可以帮助你复用复杂的布局。

5.1 Imports

Imports可以让你在layout文件里引用class,就像在managed code中一样。data标签中可以导入多个import。以下代码示例,将View类导入布局文件:

<data>
    <import type="android.view.View"/>
</data>

// import view类之后,就可以在binding表达式中引用它了
// 下面展示了引用View的常量,VISIBLE和GONE
<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
5.1.1 Type aliases

当两个类的类名冲突时,其中一个类需要用一个alias别名来重命名(两个完整路径不同的类,类名是有相同的可能性的)。主要是为了区分。

<import type="android.view.View"/>
<import type="com.example.real.estate.View"
        alias="Vista"/>

设置之后,我们就可以使用Vista来引用com.example.real.estate.View类,而View类就是来引用android.view.View类。

5.1.2 Import other classes
  1. 导入的类型可以用作变量和表达式中的类型引用。以下示例展示了用User和List作为变量类型:
<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

注意:Android Studio尚未处理导入时,导入变量的自动完成功能可能无法在IDE中运行。你可以在定义variable的时候使用完整路径名,来变异和运行程序。

  1. 我们也可以使用import进来的类型来强转表达式中的某一部分。以下示例将connection属性强制转换为User类型。
<TextView
   android:text="@{((User)(user.connection)).lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
  1. 表达式中也可以使用imported类型来引用static变量和static方法。下面的代码中import了MyStringUtils类,并且引用了capotalize方法。
<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"/>

就像managed code里一样,java.lang.*会自动被import,可以直接使用。

5.2 Variables

您可以在data元素(element)中使用多个variable元素。每个variable元素作为一个属性被设置到layout文件里,然后可以在binding表达式里使用。以下示例声明了user,image,note三个variable:

<data>
    <import type="android.graphics.drawable.Drawable"/>
    <variable name="user" type="com.example.User"/>
    <variable name="image" type="Drawable"/>
    <variable name="note" type="String"/>
</data>

在编译时检查变量类型,因此如果变量实现Observable接口或是Observable collection,那么应该反映在类型中。如果变量是未实现Observable接口的基类或接口,则不会观察变量。

当存在用于各种配置的不同布局文件(例如,横向或纵向)时,variables变量会被合并在一起。这些layout文件之间必须保证不存在冲突的变量定义。

自动生成的binding class中针对每个variable都有setter和getter方法。variable在调用对用的setter方法设置之前,都是使用默认值。引用类型就设为null,int型就是0,boolean就是false,等等。

此外,binding class还会生成还有一个很特殊的context variable,可以在binding表达式里使用。context的值是来自顶层view的getContext方法的Context类对象。context变量会被对应名称的现实变量声明给覆盖掉。

5.3 Includes

通过使用app命名空间和定义的variable名,variables可以从自身所在的布局,被传递到使用include引入的布局绑定中。下面的例子展示了name.xml和contact.xml文件使用user变量:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </LinearLayout>
</layout>

注意,data binding不支持把include的布局作为merge元素中的直接的孩子节点。比如下面的布局代码就是不支持的:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:bind="http://schemas.android.com/apk/res-auto">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <merge><!-- Doesn't work -->
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260