框架组件(二)Data Binding 2-布局与绑定表达式

2018-11-23  本文已影响0人  HelloBird_

该系列文章是对Android推出的架构组件相关文章,按作者自己理解来翻译的,同时标记有作者自己一些简单笔记。如果读者发现文中有翻译不准确的地方,或者理解错误的地方,请不吝指教。

源自Google官方
Data Binding Library
一文的翻译与归纳

其他相关链接:
Android Jetpack Components

[TOC]

表达式语言允许你使用表达式处理View调度的方法。Data Binding 库自动生成将布局中view与数据对象绑定的类。

Data Binding 布局文件与普通布局略有不同,根布局使用 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>

data 中的 user variable 描述了该布局可以使用的一个属性

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

布局中的表达式使用@{}语法来定义属性值。 TextView设置为 user 变量的 firstName 属性。

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

Note: 布局表达式需要保持短小简介,因为他们不能进行单元测试,且IDE支持功能也有限。你可以使用自定义 binding adapter 来简化布局表达式。

数据对象

假设我们现在有一个传统的 User 实体对象类:

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

这种类型对象通常用于读取一次之后不会再改变的数据,也可能会遵循一些约定,比如java中的访问器方法。如下示例所示。

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

从数据绑定的角度来看,上面两个类是等价的。@{user.firstName}表达式会使用前者的 firstName 属性和后者的 getFirstName() 方法给 android:text 属性赋值。另外,如果存在 firstName() 方法,它也可以正常解析。

绑定数据

每个布局文件都会生成一个绑定类。默认情况下,生成的类基于布局名称使用驼峰命名并添加Binding后缀来命名。之前的布局文件名称是 activity_main.xml,所以对应生成类是ActivityMainBinding。该类包含所有从布局data属性到布局view的所有绑定,并知道如何为绑定表达式赋值。推荐在布局引入创建绑定,以下是实例:

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

在运行app时,将会在界面中显示 Test 用户。另外,你可以使用 LayoutInflater 获取视图,如下所示:

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

如果在 FragmentListView 或者 RecyclerView 适配器中使用数据绑定,你可能更喜欢使用绑定类或者 DataBindingUtlinflate() 方法,如下所示:

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

表达式语言

常见特征

表达式语言看起来很像是代码中的表达式。你可以在表达式中使用以下运算符和关键字:

例子:

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

缺少的操作符

以下是可以再代码中使用但是表达式不支持的操作符:

Null合并 操作符

?? 操作符会在左侧不为空时选择前者,左侧为空时选择右侧。

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

该操作等价于:

android:text="@{user.displayName != null ? user.displayName : user.lastName}"

属性引用

一个表达式可以使用以下格式引用类属性,对于 fieldsgettersObservableField 都是一样的格式:

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

避免空指针异常

生成的数据绑定代码会自动检测 null 值,避免了空指针异常。举个例子,在表达式 @{user.name} 中,如果 user 为空,user.name 将默认分配为 null 值。如果你引用的是 user.age,其中age是 int 类型,那么数据绑定时将默认使用0。

集合

常见集合,例如 array、list、SparseArray、与 map等,都可以使用 [] 操作符访问其中的元素。

<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 中的元素。举个例子,在上面例子中的 @{map[key]} 可以替换为 @{map.key}

字符串文本

你可以使用单引号将属性值括起来,这将允许你在表达式中使用双引号来表示字符串:

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

当然也可以用双引号将属性值括起来。这时字符串文本使用单引号来表示:

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

资源

你可以用以下表达式语法来分配resource:

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

可以通过提供参数来格式化字符串或复数:

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

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


  Have an orange
  Have %d oranges

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

一些资源需要指定明确的类型,如下所示:

类型 正常引用 表达式引用
String[] @array @stringArray
int[] @array @intArray
TypedArray @array @typedArray
Animator @animator @animator
StateListAnimator @animator @stateListAnimator
color int @color @color
ColorStateList @color @colorStateList

事件处理

Data binding 允许你使用表达式来处理 view 分发的时间(例如 onClick 方法)。时间属性名称由 Listener 方法来决定,但有一些例外。比如 View.OnClickListener 有一个 onClick() 方法,所以该事件对应属性值是 android:onClick

这里有一些特殊的点击事件处理需要用 android:onClick 之外以属性避免冲突。如下所示:

设置监听 属性
SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

你可以用以下机制处理事件:

方法引用

事件可以直接绑定到方法上,同样的,android:onClick 可以关联 activity的一个方法。和 View 的 onClick 属性相比,一个主要的优点是表达式是在编译时处理,所以如果方法不存在或者格式不对,会直接提示编译错误。

方法引用和listener绑定主要不同点是,当数据绑定时才创建实际的 listener 实例,而不是触发事件时。如果你希望事件触发时计算表达式,你应该使用 listener 绑定。

要将事件分配给处理者,需要使用不同绑定表达式,即值设置为要调用的方法名称。如下示例:

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

如下设置,绑定表达式可以将 view 单击监听分配给 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 重写的方法格式、参数完全一致。

Listener 绑定

Listener 绑定是在事件发生时运行绑定表达式。和方法引用十分类似,但是它们可以让你运行任意数据绑定表达式。这个功能只适用于Gradle版本2.0及更高版本。

在方法引用中,指定方法的参数必须与事件 listener 方法参数匹配。在 listener 绑定中,只需要你的返回值与 listener 返回值匹配(或者返回值是void)。举个例子,思考下面 有 onSaveClick 方法的 presenter 类。

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

然后你可以像下面这样绑定 click 事件到 onSaveClick() 方法上:

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

在表达式中使用 callback 时,data binding 自动为事件创建必要的 listener。当 view 触发事件时,data binding 计算对应的表达式。与常规表达式一样,在计算这些监听表达式时,代码依然会判空且是线程安全的。

在前面的例子中,我们还没有定义过给 onClick 传递 view 参数。对于监听参数 Listener 绑定有两种选择:你可以忽略方法的所有参数或声明所有参数。如果你更喜欢声明参数,你可以在表达式中这样使用:

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

或者你希望使用表达式中的view参数,可以这样做:

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

你可以使用有多个参数的 lambada 表达式:

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 ,你的表达式也必须返回相同类型的值。比如,如果你监听的是 long click 事件,你的表达式必须返回一个 boolean 值。

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

如果由于 null 对象导致表达式无法计算,data binding 会返还对应类型的默认值。比如对象引用对应 nullint 对应 0boolean 值对应 false 等等。

如果你的表达式有?:??之类的计算,你可以使用 void 来作为一个元素。如下所示:

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

避免复杂监听

监听表达式很强大,且可以让你的代码更简单易懂。但另一方面,如果监听表达式复杂会让你的 layout 更难理解和维护。所以这些表单式应该仅仅用于将数据从 UI 传递到你的回调方法。你应该在回调方法中来实现业务逻辑,而不应该在监听表达式中实现。

import,variable 和 include

Data Binding 库提供了 importvariableinclude 这些功能。import 让你的布局文件更轻松的引用类;variable 允许你定义能在绑定表达式中使用的属性;include 让你可以重用复杂的布局。

imports

Import 允许你在布局文件中轻松引用类,就像在代码里一样。data 元素中可以定义0个或多个 import 元素。如下示例展示在布局文件中引入 View 类:

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

引入 View 类后允许你在绑定表达式中使用它。下面例子展示了如何使用 View 类中的 VISIBLEGONE 常量。

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

类型别名

当多各类名称冲突时,其中某些类可以定义一个别名。下面例子展示了将在 com.example.real.estate 包中的 View 类重命名为 Vista

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

这样在布局文件中就可以用 Vista 表示 com.example.real.estate.View 类,而 View 表示 android.view.View

import 其它类

引入类型可以用作变量或者表达式中的类型引用。下面例子展示了 UserList 用作变量的类型:

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List<User>"/>
</data>

你也可以在表达式中使用引入类型强制转换。就像下面的例子一样:

<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"/>

就像代码一样,java.lang.* 会被自动引入

Variable

你可以在 data 元素中使用多个 variable 元素。每个 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>

这些 variable 类型会在编译时被检测,因此如果 variable 实现了 Observable 或者 observable collection,必须在类型中体现。如果 variable 是没有实现 Observable 接口的基类或接口,将不能被观测。

当存在不同配置下的布局文件时(比如横屏、竖屏),variable 将被组合。这些布局文件之间不能存在冲突的变量定义。

生成的 binding 类中每个 variable 都有 getset 方法。在给这些 variable 设置值之前,都会使用对应类型默认值。比如引用类型为null,int 为0,booleanfalse 等等。

一个特殊的 variable 名称是 context ,为需要的表达式生成,该变量是由根 view 的 getContext() 方法获取的 Context 对象。如果要覆盖 context 变量,则需要显示声明 variable 来覆盖。

Includes

variable 可以用通过 includ 标签的 bind 属性传递到引入的布局中。下面例子展示了 name.xmlcontact.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>
上一篇下一篇

猜你喜欢

热点阅读