MVVM架构的核心就是数据驱动,数据驱动的意思就是,数据更新的时候,自动刷新UI。采用MVVM架构会节省大量的更新UI的代码,并且数据更新后主动出发UI更新这种方式,更难出错,鲁棒性更强。且不需要关注数据变化的时机,是需要关注数据变化的结果即可。
打开布局文件,选中根布局的 ViewGroup,按住 Alt + 回车键,点击 “Convert to data binding layout”,就可以生成 DataBinding 需要的布局规则
package com.peng.databindingdemo.model
data class User(val Name: String, val Age: String)
对象的属性必须可以直接访问,或者提供访问方法。
接下来,在Layout布局文件的"data"标签当中声明要使用到的变量、类的全路径。 如下
也可以采用「import」的方式引进来,这样我们就不用每次都指明整个包名的路径了:
如果我们「import」相同,我们还可以采用增加「alias」字段来指定别名:
布局中使用 @{} 来设置属性
系统会自动为我们的绑定布局生成绑定类,规则是布局文件名+Binding,例如上面的布局文件名是:activity_main.xml,那么自动生成的绑定类就是TestActivityDataBindingBinding。该类包含布局属性到布局视图的所有绑定,并知道如何为绑定表达式指定值。
class DataBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity_data_binding)
val viewDataBinding: TestActivityDataBindingBinding =
DataBindingUtil.setContentView(this, R.layout.test_activity_data_binding)
viewDataBinding.title = "三国演艺"
viewDataBinding.listData = mutableListOf("刘备", "曹操", "孙权")
viewDataBinding.mapData = mutableMapOf(0 to "赵云")
viewDataBinding.userInfo = User("关羽", "52")
}
}
如果是在其他地方绑定不觉也可以使用如下两种方式进行布局绑定:
ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// 或者
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
对于 include 的布局文件,一样是支持通过 dataBinding 来进行数据绑定,此时一样需要在待 include 的布局中依然使用 layout 标签,声明需要使用到的变量
view_include.xml:
在主布局文件中将相应的变量传递给 include 布局,从而使两个布局文件之间共享同一个变量
DataBinding 支持在布局文件中使用以下运算符、表达式和关键字:
Databinding事件绑定,分两种方式:方法引用和监听绑定
public class UserPresenter {
public void onUserNameClick(User user) {
Toast.makeText(Main5Activity.this, "用户名:" + user.getName(), Toast.LENGTH_SHORT).show();
}
public void afterTextChanged(Editable s) {
user.setName(s.toString());
activityMain5Binding.setUserInfo(user);
}
public void afterUserPasswordChanged(Editable s) {
user.setPassword(s.toString());
activityMain5Binding.setUserInfo(user);
}
}
方法引用的方式与调用函数的方式类似,既可以选择保持事件回调方法的签名一致:@{userPresenter.afterTextChanged},此时方法名可以不一样,但方法参数和返回值必须和原始的回调函数保持一致。也可以引用不遵循默认签名的函数:@{()->userPresenter.onUserNameClick(userInfo)},这里用到了 Lambda 表达式,这样就可以不遵循默认的方法签名,将userInfo
对象直接传回点击方法中。此外,也可以使用方法引用 :: 的形式来进行事件绑定
方法引用和监听器绑定之间的主要区别在于实际监听器实现是在绑定数据时创建的,而不是在事件触发时创建的。
public class StringUtils {
public static String toUpperCase(String str) {
return str.toUpperCase();
}
}
当在布局中使用属性绑定表达式时,每当绑定的变量值发生更改时,生成的绑定类必须使用绑定表达式调用 View 上的 setter方法. Data Binding 允许你为任何 setter 方法创建绑定属性。
对于名为 example 的属性,DataBinding 库会自动尝试查找接受兼容类型作为参数的 setExample(arg)方法,例如android:text="@{user.name}"
,DataBinding 库将查找 user.getName() 返回的值设置给 setText(arg) 方法;
DataBinding默认可以在布局中使用具有setter方法的View的属性, 但是如果有些 View 属性具有不按属性名匹配的 setter 方法,在这种情况下你可以使用@BindingMethods 注解来关联对应的 setter 方法,需要通过创建一个自定义属性来关联一个类中已有的方法,注解是写在一个类上面,它可以包含多个 @BindingMethod 注解,每个注解对应着一个属性的关联方法。
@BindingMethods({@BindingMethod(type = android.widget.TextView.class, attribute = "myBackgroundColor",
method = "setBackgroundColor")})
public class MyTextView extends AppCompatTextView {
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
这里为TextView的setBackgroundColor方法设置了另外的一个属性名叫做myBackgroundColor,它的命名空间是app。如果想设置为android就需要在在前面显示的设置android命名空间。如:”android:myBackgroundColor”
可以看到在MyTextView的属性设置中我们使用了myBackgroundColor属性。
大多数情况下,你不需要写这样的注解,因为大多数 View 的属性都有相匹配的 setter 方法,它会以自动选择方法的方式找到。
有些属性需要自定义绑定逻辑。 例如,ImageView 的 android:src 属性,它没有相匹配的 setter 方法,但它有 setImagexxx 方法。 可以使用带有@BindingAdapter 注解的静态绑定适配器方法来达到自定义调用 setter 方法。
public class BindAdapter {
@BindingAdapter("app:image")
public static void bindImage(ImageView view, int resId) {
view.setImageResource(resId);
}
}
这个自定义方法名可以是任意的。 第一个参数确定与该属性关联的 View 的类型,也就是说为 ImageView 声明了一个 app:image 属性。 第二个参数确定给定属性的绑定表达式中接受的类型。
这样一个自定义逻辑的绑定方法就写好了,可以在方法中自定义任何逻辑,当有一些重复繁琐的操作时,很合适写一个自定义逻辑绑定适配器。 上面讲的@BindMethods都是建立在已有的方法之上的。通过@BindAdapter还可以自定义属性名以及它所做的逻辑。
@BindingAdapter(value = { "imageUrl", "error" }, requireAll = false)
public static void loadImage(ImageView view, String url, Drawable error) {
Glide.with(view.getContext()).load(url).into(view);
}
可以看到命名空间可以随意, 但是如果在BindingAdapter的数组内你定义了命名空间就必须完全遵守
在上文中讲到,通过DataBinding进行绑定控件以及进行相关操作,但是绑定的变量发生变化的时候,每次都要重新向 ViewDataBinding 传值进行更新操作之后才能刷新UI。Observable 就是为此而生的概念,通过Observable为当数据改变时同时使视图刷新。
一个类里面的属性被更新后,UI并不会自动更新。而数据绑定后,自然会期望数据变更后 UI 会即时刷新,Observable 就是为此而生的概念。DataBinding的本身是对View层状态的一种观察者模式的实现,通过让View与ViewModel层可观察的对象进行绑定,当ViewModel层数据发生变化,View层也会自动进行UI的更新。BaseObservable 提供了 notifyChange() 和 notifyPropertyChanged() 两个方法,前者会刷新所有的值域,后者则只更新对应 BR 的 flag,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图。
public class Person extends BaseObservable {
//如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
@Bindable
public String name;
//如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
private String age;
public Person(String name, String details) {
this.name = name;
this.age = details;
}
public void setName(String name) {
this.name = name;
//只更新本字段
notifyPropertyChanged(BR.name);
}
@Bindable
public String getAge() {
return age;
}
public void setAge(String details) {
this.age = details;
//更新所有字段
notifyChange();
}
}
在 setName() 方法中更新的只是本字段,而 setAge() 方法中更新的是所有字段。
class SingleDataBindingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity_single_data_binding)
val viewDataBinding: TestActivitySingleDataBindingBinding =
DataBindingUtil.setContentView(this, R.layout.test_activity_single_data_binding)
val person = Person("关羽", "52")
viewDataBinding.personInfo = person
findViewById
继承于 Observable 类相对来说使用起来会复杂一些,且需要自己进行 notify 操作才能刷新UI,因此为了简单起见可以选择使用 ObservableField。ObservableField 可以理解为官方对 BaseObservable 中字段的注解和刷新等操作的封装,官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble以及 ObservableParcelable ,也可通过 ObservableField 泛型来申明其他类型
public class UserBean {
public ObservableField name;
public ObservableInt number;
public UserBean(ObservableField name, ObservableInt number) {
this.name = name;
this.number = number;
}
}
class ObservableFieldActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity_observable_field)
val viewDataBinding: TestActivityObservableFieldBinding =
DataBindingUtil.setContentView(this, R.layout.test_activity_observable_field)
val user = UserBean(ObservableField("赵云"), ObservableInt(10086))
viewDataBinding.userBean = user
findViewById
对 ObservablePerson 属性值的改变都会立即触发 UI 刷新,概念上与 Observable 区别不大。
dataBinding 也提供了包装类用于替代原生的 List
和 Map
,分别是 ObservableList
和 ObservableMap
,当其包含的数据发生变化时,绑定的视图也会随之进行刷新。
双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据,上文中我们实现了数据改变时同时使视图刷新,下面探讨视图改变时同时改变数据,数据绑定框架已经为常用的双向绑定属性和属性改变监听器提供了双向绑定的实现。android:text属性就是其中之一。
public class StaffBean {
public ObservableField name;
public String code;
public StaffBean(ObservableField name, String code) {
this.name = name;
this.code = code;
}
public ObservableField getName() {
return name;
}
public String getCode() {
return code;
}
public void setName(ObservableField name) {
this.name = name;
}
public void setCode(String code) {
this.code = code;
}
}
布局文件:
Activity中
class TwoWayBindActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.test_activity_two_way_bind)
val viewDataBinding: TestActivityTwoWayBindBinding =
DataBindingUtil.setContentView(this, R.layout.test_activity_two_way_bind)
val staffBean = StaffBean(ObservableField(""), "0")
viewDataBinding.staffBean = staffBean
}
}
通过ObservableField可以将数据的变化通知到Ui进行修改,@={}可以将UI数据的变化通知到数据类进行变化,这样就实现了数据的双向绑定.
死循环绑定:因为数据源改变会通知view刷新,而view改变又会通知数据源刷新,这样一直循环往复,就形成了死循环绑定 看看dataBinding源码中是如何解决的,路径:android.databinding.adapters.TextViewBindingAdapter:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
从上面的源码可以看到,在处理双向绑定的业务逻辑时,要对新旧数据进行比较,只处理新旧数据不一样的数据,对于新旧数据一样的数据作return处理,通过这种方式来避免死循环绑定。
参考资料:
DataBinding官方文档