Android-MVVM之DataBinding

MVVM一种android架构模式,谷歌官方架构中实现了,包括databinding,viewmodel,livedata,room,lifecycle,等一套用于mvvm架构的架构组件。

前言

整体学习MVVM之前,我们可以先来单独看看DataBinding是干嘛的

首先附上官方文档地址:Data Binding Library ,以及官方demo

官方是这么说的:

数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。

就像下面这样:

1
<TextView android:text="@{viewmodel.userName}" />

诶,好像似曾相识,对了,和那个jsp的el表达式,以及spring中表达式有点像啊。这样就好理解了,xml UI通过某种数据绑定,可以通过这种声明式将数据传到UI中。不得不说有点亲切,哈哈。我下面的写东西也基本上是总结(翻译233)官方文档,所以结构也是一样的。

初步

API要求14以上,gradle1.5以上

构建环境

开启DataBinding

1
2
3
4
5
6
android {
...
dataBinding {
enabled = true
}
}

AS对databinding的支持

  • 语法高亮
  • 语法错误标记
  • xml代码完成
  • 引用定位至导航和文档

布局和绑定表达式

这里开始啦啊

包含DataBinding的layout

在原本基础上外层套上以layout标签,以及内部紧跟着声明data标签,在data标签使用variable里面就可声明绑定变量了。在xml中使用绑定数据通过@{user.firstName}的形式使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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>

声明数据对象

使用pojo简单对象,或者老套路,get/set准备,然后@{user.firstName}会去调用getFirstName()或者firstName(),或者直接访问前提是public的firstName

1
2
3
4
5
6
7
8
public class User {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

绑定数据

通过上述声明layout文件,开始构建,会生成以xml文件开头,尾缀为Binding的类,比如ActivityMainBinding.java,下面看下如何将对象绑定到layout文件中的

1
2
3
4
5
6
7
@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);
}

这样一来数据就和layout绑定好了,layout就能通过表达是显示绑定数据

使用LayoutInflater获取binding

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

如果在FragmentListViewRecyclerView adapter中使用databinding,可以这样

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

表达式语言

通用表达

表达式语言和代码很相似,可以使用以下的操作符和关键字

  • 算术符:+ - / * %
  • 字符串连接符:+
  • 逻辑符&& ||
  • 位操作符:& | ^
  • 一元运算符:+ - ! ~
  • 移位符:>> >>> <<
  • 比较符:== > < >= <=(Note that<needs to be escaped as<`)
  • instanceof
  • 括号()
  • 字面量: character, String, numeric, null
  • 类型转换
  • 方法调用
  • 属性访问
  • 数组访问 []
  • 三元符 ?:

例如:

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

不可以用的操作符

  • this
  • super
  • new
  • Explicit generic invocation (我理解位泛型调用行吗?)

空合并运算符

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

等价于

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

属性访问

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

防止空指针异常

生成的绑定代码自动检查null以及防止空指针异常,比如@{user.name}中如果user为空,那么name会被分配为null,如果是@{user.age},则age分配为0

运用集合

自然是少不不了集合了的使用了,数组,list,sparse list ,map可以通过[]形式访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<data>
<import type="android.util.SparseArray"/>
<import type="java.util.Map"/>
<import type="java.util.List"/>
<variable name="list" type="List&lt;String>"/>
<variable name="sparse" type="SparseArray&lt;String>"/>
<variable name="map" type="Map&lt;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]}"

注意:像List<String>这种带泛型需要写成List&lt;String>,其它的依次类推

字符串字面量key

下面的两种形式也是可以的,通过字面量访问值,双引号,反引号

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

访问资源

结合表达式来访问资源

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

格式化字符串

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

默认值

1
2
3
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName, default=my_default}"/>

事件处理

Data Binding允许通过表达式来处理分发的view事件,例如点击事件,需要在onclick属性上写表达式。有三种需要特殊处理

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

有两种方式来处理事件:

  • 方法引用:通过调用具有相同签名的方法
  • 监听器绑定: 通过lambda方式书写对象

方法引用

类似于书写onClick属性关联到activity的同签名方法,不过这个方法引用会在编译时处理,即如果出现签名不一致等错误,会在编译时报错。签名必须与触发事件一致

声明处理器

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

调用,:: .两种都行,最好别用吧,因为被废弃了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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>

监听器绑定

实际处理器

1
2
3
public class Presenter {
public void onSaveClick(Task task){}
}

通过@{() -> presenter.onSaveClick(task)}类似lambda表示式

1
2
3
4
5
6
7
8
9
10
11
<?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>

监听器的实现是数据绑定时被创键的,和方法引用类似,但是监听器绑定的处理方法不要求签名完全一致,只需要保证返回值一致就行了

当然你也可以使用view参数

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

也可以继续传入到处理方法中

1
2
3
4
5
public class Presenter {
public void onSaveClick(View view, Task task){}
}

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

当然某些事件不止一个参数

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

结合三元表达式使用默认返回值,null0

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

使用过程要保证监听器简洁,不宜过度复杂。

import 和 variable,includes

import

和java文件中的导包一样。

1
2
3
4
5
6
7
8
<data>
<import type="android.view.View"/>
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

使用别名,类名冲突时使用别名

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

反正和java中怎么导入怎么用基本一样,包括了静态方法,嗯,真香

1
2
3
4
5
6
7
8
9
<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"/>

variable

声明变量,这些变量都是会在编译时进行类型检查的。binding会为每个变量分配默认值,参考java类属性默认值。

有一个特殊的变量context,即view.getContext

include

传递变量到其它布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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>

不支持直接的merge子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>

使用可观察对象

databinding库允许数据是可观察的。在可观察性下,数据发生改变会自动更新与之绑定的UI数据。当然后续会使用自带生命周期感知的LiveDta来替换Obervable增强功能

Observable fields

当你不需要观察整体时,考虑使用可观察字段,有以下的选项

1
2
3
4
5
6
7
8
9
10
11
12
ObservableBoolean
ObservableByte
ObservableChar
ObservableShort
ObservableInt
ObservableLong
ObservableFloat
ObservableDouble
ObservableParcelable
ObservableField<T>
ObservableArrayMap
ObservableArrayList

声明为final

1
2
3
4
5
private static class User {
public final ObservableField<String> firstName = new ObservableField<>();
public final ObservableField<String> lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}

通过get,set访问

1
2
user.firstName.set("Google");
int age = user.age.get();

Observable objects

整体声明为可观察对象,通过继承BaseObservable,通过在set方法中调用notifyPropertyChanged();来通知UI变化,对了get方法需要使用@Bindle来注解,不然无法从BR找到字段资源ID,这个ID是和UI关联的,通过ID定位到UI数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static class User extends BaseObservable {
private String firstName;
private String lastName;
@Bindable
public String getFirstName() {
return this.firstName;
}
@Bindable
public String getLastName() {
return this.lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(BR.lastName);
}
}

生成绑定类

可以定制化的生成banding类

创键banding类

通过inflate注入

1
2
3
4
5
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
}

添加到父viewgroup

1
MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false);

绑定view

1
MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);

不知类型的情况下绑定view

1
2
View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ViewDataBinding binding = DataBindingUtil.bind(viewRoot);

在fragment,listview,recyclerview adpter,优先使用inflate

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

setContentView

1
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

通过id定位view

以前没有databinding时总是需要手动findViewById,或者通过ButterKnife来辅助,现在有了DataBinding后可以直接通过xxxBinding.id的形式来获得view对象(不过用kotlin来写话的天然直接通过id引用对象233)

1
2
3
4
5
<Button
android:text="to viewmodel activity"
android:id="@+id/btn_to_viewmodel_activity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

会将下划线式的命名规则转变为小驼峰式

1
binding.btnToViewmodelActivity.setOnclcikListener();

立即执行绑定

原本在可观察数据变化时,会在下一帧进行ui变化,如果你想强制立即执行调用 executePendingBindings()

动态变量

例如在RecyclerView.Adapter中你可能不知道Banding是那个。通过以下方式通知修改数据

1
2
3
4
5
public void onBindViewHolder(BindingHolder holder, int position) {
final T item = items.get(position);
holder.getBinding().setVariable(BR.item, item);
holder.getBinding().executePendingBindings();
}

多线程问题

在databinding中,修改数据可以在任何线程,数据在UI的上的改变会自动在本地化的UI线程进行更新。需要注意的是不能在后台线程操纵集合数据

自定义binding类的名字

1
2
3
<data class="ContactItem">

</data>

绑定适配器

绑定适配负责通过适当的方式来调用并且设置值,比如我们在设置属性的时候实际是通过setText来完成的,设置监听器是通过setOnclickListener来完成的。databing库允许我们指定方法调用来设置值,提供绑定逻辑,指定返回类型。

设置属性值

自动选择方法

通过名称和返回值类型来匹配对应的方法调用。比如app:scrimColor=@{@color/scrim}会调用setScrimColor(int)诸如此类,自动选择方法调用

指定属性触发方法

1
2
3
4
5
6
@BindingMethods({
@BindingMethod(type = "android.widget.ImageView",
attribute = "android:tint",
method = "setImageTintList"),
})
class MyBindingMethods
1
2
3
4
5
@BindingMethods(
@BindingMethod(type = ImageView.class,
attribute = "app:srcCompat",
method = "setImageResource"))
class MyBindingMethods

在这样的声明下,通过给app:srcCompat设置属性值,会触发调用setImageResource方法

自定义触发方法逻辑

设置android:paddingLeft就会调用自定义的setPaddingLeft

1
2
3
4
5
6
7
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
view.setPadding(padding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}

同样支持自定义属性,和多属性

1
2
3
4
@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
Picasso.get().load(url).error(error).into(view);
}
1
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" />

databing 库会忽略自定义属性的命名空间,所以不写也没事

可以设置不要求所有属性有值

1
2
3
4
5
6
7
8
@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

提供新值,旧值参数

1
2
3
4
5
6
7
8
9
@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
if (oldPadding != newPadding) {
view.setPadding(newPadding,
view.getPaddingTop(),
view.getPaddingRight(),
view.getPaddingBottom());
}
}

甚至新旧监听器参数

1
2
3
4
5
6
7
8
9
10
11
12
@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
View.OnLayoutChangeListener newValue) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (oldValue != null) {
view.removeOnLayoutChangeListener(oldValue);
}
if (newValue != null) {
view.addOnLayoutChangeListener(newValue);
}
}
}

然后使用监听器绑定

1
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

对于一个监听器中有多个监听方法的需要拆分出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewDetachedFromWindow {
void onViewDetachedFromWindow(View v);
}

@TargetApi(VERSION_CODES.HONEYCOMB_MR1)
public interface OnViewAttachedToWindow {
void onViewAttachedToWindow(View v);
}


@BindingAdapter({"android:onViewDetachedFromWindow", "android:onViewAttachedToWindow"}, requireAll=false)
public static void setListener(View view, OnViewDetachedFromWindow detach, OnViewAttachedToWindow attach) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1) {
OnAttachStateChangeListener newListener;
if (detach == null && attach == null) {
newListener = null;
} else {
newListener = new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (attach != null) {
attach.onViewAttachedToWindow(v);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if (detach != null) {
detach.onViewDetachedFromWindow(v);
}
}
};
}

OnAttachStateChangeListener oldListener = ListenerUtil.trackListener(view, newListener,
R.id.onAttachStateChangeListener);
if (oldListener != null) {
view.removeOnAttachStateChangeListener(oldListener);
}
if (newListener != null) {
view.addOnAttachStateChangeListener(newListener);
}
}
}

对象转换

一般情况下会有自动转换,比如android:text="@{String}",string就会转换为CharSequence以供setText使用

我们也可以使用自定义对象转换比如:

1
2
3
4
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

我们的参数值是一个int,但是最后起作用的是Drawable,所以我们需要将int转换为ColorDrawable

1
2
3
4
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}

这样就会执行转换

结合架构组件

使用LiveData

和ObservableField相似,LiveData数据也是可观察的,但是LiveData更加优秀的是感知生命周期,即在某些不应该触发通知的生命周期中是不会触发通知的。要求AS版本3.1以上,详情见之后的LiveData介绍

使用LiveData需要设定生命周期的承载者来保持LiveData作用

1
2
3
4
5
6
7
8
9
class ViewModelActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// Inflate view and obtain an instance of the binding class.
UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);
// 指定生命周期承载者是此activity
binding.setLifecycleOwner(this);
}
}

然后再数据类中使用

1
2
3
4
5
6
7
8
class ScheduleViewModel extends ViewModel {
LiveData username;

public ScheduleViewModel() {
String result = Repository.userName;
userName = Transformations.map(result, result -> result.value);
}
}

使用ViewModel管理UI关联数据

viewModel用于管控UI行为,以及存储绑定数据,在某些屏幕旋转情况下,activity会重建,而viewmodel可以一直存在,保存于UI的关联的重量级数据,同时作为逻辑沟通的桥梁,连接UI,连接Model。具体见后续的ViewModel

ViewModel实现Obervable

在某种不需要使用LiveData进行生命周期感知的情况下,可以进行实现Observable,使得ViewModel具有被观察的特性。

首先继承ViewModel,实现Observable,添加属性监听。

重写addOnPropertyChangedCallback removeOnPropertyChangedCallback,可以参看BaseObservable的基本实现范例

这样一个ViewModel就具有观察特性了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* A ViewModel that is also an Observable,
* to be used with the Data Binding Library.
*/
class ObservableViewModel extends ViewModel implements Observable {
private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

@Override
protected void addOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.add(callback);
}

@Override
protected void removeOnPropertyChangedCallback(
Observable.OnPropertyChangedCallback callback) {
callbacks.remove(callback);
}

/**
* Notifies observers that all properties of this instance have changed.
*/
void notifyChange() {
callbacks.notifyCallbacks(this, 0, null);
}

/**
* Notifies observers that a specific property has changed. The getter for the
* property that changes should be marked with the @Bindable annotation to
* generate a field in the BR class to be used as the fieldId parameter.
*
* @param fieldId The generated BR id for the Bindable field.
*/
void notifyPropertyChanged(int fieldId) {
callbacks.notifyCallbacks(this, fieldId, null);
}
}

双向绑定

基础

终于来到大名鼎鼎的双向绑定了。之前我们的普通操作都是修改绑定数据,触发UI变化。

单向绑定过程:data set-> data notify -> UI get->UI change

双向绑定过程:UI set -> data set -> data notify -> UI get -> UI change

简单来讲就是以前是通过直接修改绑定数据来影响UI值,现在双向绑定修改UI值来触发绑定数据改变。

单向绑定

1
2
3
4
5
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@{viewmodel.rememberMe}"
android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
/>

通过reemberMeChanged来触发改变事件,继而自行书写修改绑定数据逻辑

现在双向绑定

1
2
3
4
<CheckBox
android:id="@+id/rememberMeCheckBox"
android:checked="@={viewmodel.rememberMe}"
/>

使用@={viewmodel.rememberMe}来表示双向绑定,意思是在checkbox UI属性变化时会去调用setRememberMe方法,UI数据和绑定数据互相可以影响,但是需要通过一些判断来防止进入无限的循环调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class LoginViewModel extends BaseObservable {
// private Model data = ...

@Bindable
public Boolean getRememberMe() {
return data.rememberMe;
}

public void setRememberMe(Boolean value) {
// Avoids infinite loops.
if (data.rememberMe != value) {
data.rememberMe = value;

// React to the change.
saveData();

// Notify observers of a new value.
notifyPropertyChanged(BR.remember_me);
}
}
}

用于自定义属性

自定义属性也可以进行双向绑定

使用@BindingAdapter绑定set方法

1
2
3
4
5
6
7
@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
// Important to break potential infinite loops.
if (view.time != newValue) {
view.time = newValue;
}
}

使用@InverseBindingAdapter绑定get方法

1
2
3
4
@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
return view.getTime();
}

设置同用的监听

1
2
3
4
5
@BindingAdapter("app:timeAttrChanged")
public static void setListeners(
MyView view, final InverseBindingListener attrChange) {
// Set a listener for click, focus, touch, etc.
}

转换

在双向绑定中的转换就会涉及到反向转换,比如原来我们是date to string,现在反向转换就是string to date了

1
2
3
4
5
6
7
8
9
10
11
12
public class Converter {
@InverseMethod("stringToDate")
public static String dateToString(EditText view, long oldValue,
long value) {
// Converts long to String.
}

public static long stringToDate(EditText view, String oldValue,
String value) {
// Converts String to long.
}
}

xml中

1
2
3
4
<EditText
android:id="@+id/birth_date"
android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

避免无限循环

在双向绑定中因为涉及两个值循环变化,需要通过比较值来,避免无限循环的调用

databing 提供的双向绑定属性

这个就是说它内部实现一些双向绑定的Adapter,就是不用我们自己写,当然自定义的属性双向绑定还是得自己写咯

我就直接从官网拷啦

Class Attribute(s) Binding adapter
AdapterView android:selectedItemPosition android:selection AdapterViewBindingAdapter
CalendarView android:date CalendarViewBindingAdapter
CompoundButton android:checked CompoundButtonBindingAdapter
DatePicker android:year android:month android:day DatePickerBindingAdapter
NumberPicker android:value NumberPickerBindingAdapter
RadioButton android:checkedButton RadioGroupBindingAdapter
RatingBar android:rating RatingBarBindingAdapter
SeekBar android:progress SeekBarBindingAdapter
TabHost android:currentTab TabHostBindingAdapter
TextView android:text TextViewBindingAdapter
TimePicker android:hour android:minute TimePickerBindingAdapter

这些都是已经帮你写好得双向绑定属性

结语

总结下来就是,可以将数据绑定到UI页面的操作。方便了书写,不用写过多的样版代码,同时也解耦了,耦合性高的都被APT自动生成了,不用我们管。反正写的时候我还是遇见了各种问题,尤其是用Kotlin写的时候,一样的代码都跑不通,kotlin还是得好好琢磨琢磨啊。这个文章基本算是简单得翻译了官方文档吧,有时间还是写个demo好了。