[译] 高级 DataBinding: 绑定到 LiveData (单向绑定 & 双向绑定)

声明: 本文翻译自Medium 文章

数据绑定库在 Android 应用程序的生产中越来越受欢迎,因为它扩大了 MVVM 架构的优势。它允许您使用声明性格式而不是以编程方式将布局中的UI组件绑定到应用程序中的数据源。自动通知UI关于数据的变化并获得传播回来的UI属性的变化,这不是很好吗?那么让我们看看我们如何通过单向和双向绑定绑定到 LiveData !

为什么绑定到 LiveData?

使用生命周期感知组件(如LiveData)的优点包括:

  • 没有因停止活动而崩溃。如果观察者的生命周期处于非活动状态,例如活动位于后台堆栈中,则它不会收到任何 LiveData 事件。

  • 自适应的配置更改。如果由于配置更改(例如,设备旋转)而重新创建 Fragment 或 Activity ,则会立即接收最新的可用数据。

  • 没有内存泄漏。无需手动处理订阅。当相关的生命周期被破坏时,观察者会自行清理。

LiveData VS ObservableFields

与实现 Observable 的对象 (例如可观察字段)不同  ,  LiveData 对象知道订阅数据更改的观察者的生命周期。无论 LiveData 和 ObservableFields, 他们都可以实现其观察数据变化,但是在其他方面,LiveData 有它特殊的优势:

  • 手动生命周期处理。UI组件只是观察相关数据,不会停止或恢复观察。LiveData 自动管理所有这些,因为它在观察时意识到相关的生命周期状态变化。

  • Transformations 和 MediatorLiveData 的更多功能。使用 LiveData 将使您受益于转换的强大功能,并且还可以向 MediatorLiveData 添加多个源。因此,如果布局中有5个 EditText 视图,则无需从 Activity 或 Fragment 中观察全部 5个 视图。您只能观察一个 MediatorLiveData,这将为您节省一些代码和逻辑复杂性。

  • 共享资源。创建扩展 LiveData 的自定义对象将允许您连接到系统服务一次,然后任何需要该资源的观察者只能观察该对象。

开始使用带数据绑定的 LiveData

要将 LiveData 对象与绑定类一起使用,需要指定生命周期所有者以定义 LiveData 对象的范围。以下示例说明如何在实例化绑定类之后将活动设置为生命周期所有者:

1
2
3
4
// Inflate view and obtain an instance of the binding class.
val binding: MainBinding = DataBindingUtil.setContentView(this, R.layout.main)
// Specify the current activity as the lifecycle owner.
binding.setLifecycleOwner(this)

所以现在我们可以在布局文件 main.xml 中使用 LiveData 对象,如下所示,commentText 的值将设置到 text 属性上:

1
2
<android.support.design.widget.TextInputEditText
android:text="@{viewModel.commentText}" />

在某些情况下,在数据绑定中使用 LiveData 时,可能会出现警告“…是一个盒装字段但需要取消装箱以执行…”。这表示使用可空类型作为 LiveData 对象值。要禁止警告,建议使用原语( ObservableInt 而不是 MutableLiveData )或使用safeUnbox,如下所示:

1
android:text="@{safeUnbox(viewModel.commentText)}"

实现双向绑定

在预期从 UI 更新 LiveData 值的情况下,双向绑定变得非常方便。在代码中访问它时,我们希望收到更新的值。为了能够做到这一点,我们将在绑定表达式的花括号之前添加“=”:

1
2
<android.support.design.widget.TextInputEditText
android:text="@={viewModel.commentText}" />

现在,只要用户在屏幕上的视图中键入新文本,LiveData 对象就会更新,当访问其值时,我们将收到最新的更新。

创建自定义绑定适配器

为了更进一步,让我们想一个不那么通用的案例。想象一下,我们希望通过使用 LiveData 对象的数据绑定在 ViewPager 中设置当前选项卡。为此,我们需要在 BindingAdapter 的帮助下为 ViewPager 创建自定义属性 currentTab :

1
2
3
4
5
6
7
8
9
10
11
12
companion object {
@BindingAdapter("currentTab")
@JvmStatic
fun setNewTab(pager: ViewPager, newTab: MutableLiveData<Int>) {
newTab.value?.let {
//don't forget to break possible infinite loops!
if (pager.currentItem != itemLiveData.value) {
pager.setCurrentItem(newTab.value, true)
}
}
}
}

所以现在我们可以将新属性添加到布局文件中,并从 LiveData 对象值设置 ViewPager 的当前项:

1
2
<android.support.v4.view.ViewPager
app:currentTab="@{viewModel.pagerCurrentTab}"/>

当新值设置为 pagerCurrentTab 对象时,将执行 BindingAdapter 正文中的代码。

使用自定义属性的双向绑定

现在,在更新我们创建的 LivaData 对象中的值时,ViewPager 会滚动到新位置。这很好,除了在我们的用例中,用户还与 UI 交互并更改ViewPager 的位置,但 LiveData 对象仍保留“旧”值。我们希望收到有关此属性更改的通知,以便基于它实现某些逻辑或只检查当前值。这可以通过实现双向绑定来实现。

我们将要对布局文件进行以下更改:

1
2
<android.support.v4.view.ViewPager
app:currentTab="@={viewModel.pagerCurrentTab}"/>

BindingAdapter我们已经有了,另外需要创建一个 InverseBindingAdapter。此时,数据绑定知道在数据更改时要做什么(它调用使用了 @BindingAdapter 注释的方法)以及在 view 属性更改时调用的内容(它调用使用了 InverseBindingListener 注释的方法)。因此,如果用户滑动 ViewPager 选项卡,LiveData 对象将使用新值进行更新。但是,为了知道属性何时或如何更改,我们引入了自定义事件。事件的命名默认为具有后缀“AttrChanged”的属性名称。在这个场景里就是 currentTabAttrChanged。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
companion object {
@BindingAdapter("currentTab")
@JvmStatic
fun setTab(pager: ViewPager, itemLiveData: MutableLiveData<Int>){
itemLiveData.value?.let {
//don't forget to break possible infinite loops!
if (pager.currentItem != itemLiveData.value) {
pager.setCurrentItem(itemLiveData.value, true)
}
}
}
@InverseBindingAdapter(attribute = "currentTab", event = "currentTabAttrChanged")
@JvmStatic
fun getTab(pager: ViewPager) = pager.currentItem

一句警告

使用双向数据绑定时,请注意不要引入无限循环。当用户更改属性时,将调用注释@InverseBindingAdapter 的方法。反过来,这将调用注释 @BindingAdapter 的方法,这将触发对注释 @InverseBindingAdapter 的方法的另一个调用,依此类推。

因此,通过比较注释 @BindingAdapter 的方法中的新旧值来打破可能的无限循环非常重要。

一些最后的想法

Android 组件的生命周期很复杂,手动管理可能很麻烦,因为保持 UI 与数据源保持同步,因此引入 LiveData 是生命周期管理中的一大步。将数据绑定添加到项目中可以使代码更加简洁和反应,因为数据源中的更改可以自动传播到 UI,同时考虑了到配置和生命周期状态。但是,使用数据绑定库不应仅限于将数据模型的属性设置为文本字段。使用单向和双向绑定绑定到 LiveData 将允许您充分利用 Observer 模式和生命周期感知。

-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!