DataBinding入门指南
简介
DataBinding即数据绑定,是Google为Android提供的一种MVVM实现方式,目前也是Android Architecture Components的一部分。
优势
-
在Binding类中生成带id的View引用,省掉Activity和Fragment中的大部分代码
-
减少xml中View的id的定义
-
绑定ViewModel与V之间的监听关系,解耦VM与V层
-
ViewModel弱引用View(Observable回调弱引用Binding类),无需关心该部分内存泄漏
-
数据可以在子线程中更新,无需关心线程切换
劣势
-
xml中的Java代码提示功能比较弱
-
报错信息没有那么直接
接入
只需要在Moudule级的build.gradle加入
android {
dataBinding {
enabled = true
}
}
基本示例
布局文件
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>`
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data class="xxx">
<import
type="com.example.databindingdemo.model.User" alias="xxx"/>
<variable
name="user"
type="User"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
app:layout_constraintTop_toBottomOf="@id/name"
android:text="@{`年龄`+user.age}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
</layout>
- 最外层必须是<layout>标签,主要的不同时上半部分多了<data>块,<data>下主要用于声明变量<variable>。
- <data class="com.example.MainBinding"> 编译时候会生成一个XXXBinding类,这个class指定生成的类名,如果不写的话使用布局文件名称的名字,比如activity_main生成ActivityMainBinding类。
- <import> 导入类,如果类名重复的话,可以使用alias给类一个别名。
- <variable>声明变量。
Activity
private ActivityMainBinding mBinding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.*activity_main);
User user=new User("Solo",19);
mDataBinding.setUser(user);
}
生成的类ActivityMainBinding,继承自ViewDataBinding,Binding类会给布局中所有带id的View生成引用,另外getRoot()方法可以获取到布局的根View。
mBinding.getRoot() //获取根View
mBinding.name //xml中定义过id的View会自动生成引用
在其他组件中获取Binding对象:
ActivityMainBinding.inflate(getLayoutInflater());
DataBindingUtil.inflate(getLayoutInflater(),R.layout.activity_main,null,false);
事件绑定
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.databindingdemo.vm.LoginViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:onClick="@{v->viewModel.onClick()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button1"
android:textSize="16sp"
/>
<Button
android:onClick="@{()->viewModel.onClick()}"
android:layout_width="wrap_content"`
android:layout_height="wrap_content"
android:text="Button2"
android:textSize="16sp" />
<Button
android:onClick="@{v->viewModel.onClick(v)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button3"
android:textSize="16sp" />
<Button
android:onClick="@{viewModel::onClick}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button4"
android:textSize="16sp" />
<CheckBox
android:onCheckedChanged="@{()->viewModel.onCheckedChanged()}"
android:layout_width="wrap_content"`
android:layout_height="wrap_content" />
<CheckBox
android:onCheckedChanged="@{(view,isChecked)->viewModel.onCheckedChanged(view,isChecked)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
事件绑定写法和lambda相似
cb.setOnCheckedChangeListener((view,isChecked)->viewModel.onCheckedChanged());
不同点:
-
后面只能有一条语句
-
语句中如果不需要用到前面参数的话,参数可以省略,如()->viewModel.onCheckedChanged()
Observable
User user=new User("Solo",19);
mBinding.setUser(user);
我们设置给user给mBinding,但是假如我们user.setName("YoYo")改变数据,这时候界面并不会自动更新。DataBinding给我们提供了一系列的Observale数据类,以便于当数据发生变化时候,可以去通知其他对象。
基本类型
ObservableBoolean,ObservableByte,ObservableChar,ObservableShort,ObservableInt,ObservableLong,ObservableFloat,ObservableDouble,另外ObservableParcelable
引用类型
集合类
ObservableArrayMap,ObservableArrayList
基类
示例
public class UserViewModel{
private UserRepo mUserRepo;
public final ObservableField<String> name = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
public UserViewModel(){
mUserRepo=new UserRepo();
}
//加载数据,然后对应的ui即会变化
pubic void loadUser(){
mUserRepo.getUser(new CallBack(){
void onSuccess(User user){
name.set(user.getName());
age.set(user.getAge());
}
void onFail(Exception e){
}
});
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import
type="com.example.databindingdemo.vm.UserViewModel"/>
<variable
name="viewModel"
type="ViewModel"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.name}"
/>
<TextView
android:text="@{`年龄`+viewModel.age}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</layout>
双向绑定
像EditText或者CheckBox,可以接受用户的输入的控件,使UI的变化也能够映射到数据。
<EditText
android:text="@={viewModel.userName}"
android:layout_width="100dp"
android:layout_height="wrap_content" />
<CheckBox
android:checked="@={viewModel.rememberMe}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
使用@={},多了=
正向映射(set)
在xml中给属性设置了@{},比如 android:text="@{年龄
+user.age}",DataBinding怎么知道调用哪个方法去设置值?
自动寻找
<com.example.databindingdemo.widget.MyTextView
app:layout_constraintTop_toBottomOf="@id/age"
app:textBold="@{true}"
android:text="加粗"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
比如上面这个例子,会去寻找setTextBold(boolean)方法(和属性动画寻找方法类似)。需要注意的是DataBinding其实并不要求这个控件本身必须有对应的属性,只要有相应的set方法就可以啦。
public class MyTextView extends AppCompatTextView{
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setTextBold(boolean bold){
getPaint().setFakeBoldText(bold);
}
}
@BindingMethod,手动指定
BindingMethod针对该类已经有适合这个属性的方法实现,不需要做什么逻辑修改。
TextViewBindingAdapter.java:
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),`
@BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
@BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
})
@BindingMethod源码:
@Target(ElementType.*ANNOTATION_TYPE*)
public @interface BindingMethod {
Class type();//哪个类,如TextView.class
String attribute();//哪个属性,如android:autoLink
String method();//调用TextView的那个歌方法
}
@BindingMethods可以声明于任何类上,包含一个BindingMethod数组。
@BindingAdapter,自定义逻辑
假如我们想要给控件现有的属性实现不了我们需要的功能的话,或者我们需要自己定义设置值过程的逻辑,可以通过BindAdapter来定义,例如:
**TextViewBindingAdapter.java:**
@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);
}
加载网络图片
@BindingAdapter(value = {"imgUrl", "options"})
public static void loadImageUseGlide(ImageView imageView, String url, RequestOptions options) {
Glide.with(imageView).load(url).apply(options).into(imageView);
}
<ImageView
android:id="@+id/image"
android:layout_width="85dp"
android:layout_height="85dp"
app:imgUrl="@{viewModel.image}" app:options="@{RequestOptions.placeholderOf(R.drawable.bg_placeholder_rent_list).centerCrop()}"
/>
给RecyclerView设置数据
@BindingAdapter({"datas"})
public static <T> void setData(RecyclerView recyclerView, List<T> list) {
BindingAdapter<T> adapter = BindingAdapter<T> recyclerView.getAdapter();
if (adapter != null && list != null) {
adapter.setData(list);
}
}
<android.support.v7.widget.RecyclerVie
android:id="@+id/rvRecommend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:datas="@{viewModel.recommends}"/>
BindingAdapter定义:
@Target(ElementType.*METHOD*)
public @interface BindingAdapter {
String[] value();//定义属性数组
boolean requireAll() default true;//是否要求所有的属性同时出现
}
需要注意的地方:
-
方法名字任意,但必须是 public static 方法
-
方法的第一个参数必须是要绑定的 View 或布局,后面的参数顺序于value数组中声明的一一对应
-
@BindingAdapter({"xxx:imageUrl"}) 中 xxx 的部分是随意写的,例如可以写成 app:imageUrl 或 bind:imageUrl 之类的都可以,不必要和 xml 中定义的相同。
-
app:imageUrl 的值必须是引用资源文件或者 java 传的对象,写作app:imageUrl="@{xxx}",而不能直接写作 app:imageUrl="xxx"。
-
我们定义的BindingAdapter优先级高于系统
反向映射(get)
控件值发生变化时候,应该调用控件的哪个方法去获取当前的值,然后再赋值给数据。
自动寻找
@InverseBindingMethod
@InverseBindingMethods({@InverseBindingMethod(
type = android.widget.TextView.class,
attribute = "android:text",
event = "android:textAttrChanged",
method = "getText")})
@InverseBindingAdapter
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
转换
自动转换
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.test}"
/>
public Object getTest() {
return test;
}
user.test="hehei"
这里test的引用类型其实是Object,但是 setText(CharSequence cs) 的参数类型是CharSequence,DataBinding会做一次类型转换。
自定义转换
这里 background 需要的一个 Drawable,但是 color 是个 int,所以需要转换成 Drawable。
<View
android:background="@{isError ? @color/red : @color/white}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这个时候需要使用BindingConversion注解,对于上面的需求DataBinding已经给帮我们实现了:
public class Converters {
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
@BindingConversion
public static ColorStateList convertColorToColorStateList(int color) {
return ColorStateList.*valueOf*(color);
}
}
表达式
支持的语法
-
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}'
- 和java一样,默认导入java.lang.*;
- 自动导入一个context变量,类型Context,通过调用根View的getContext方法获取;如果我们也定义一个context变量,会覆盖自动导入的。
不支持:
-
this
-
super
-
new
-
Explicit generic invocation (不支持泛型方法)
例如:
public class StringUtils {
public static <T> String generic(T t){
return t.toString();
}
}
<TextView
android:text="@{StringUtils.generic(user)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这样是编译不过的,不支持泛型方法,但是可以用泛型类。
??操作符
android:text="@{user.displayName ?? user.lastName}"
两句等价
android:text="@{user.displayName != null ? user.displayName : user.lastName}"
避免空指针异常
DataBinding会检查表达式,避免空指针。比如@{user.age},如果假如user为null的话,那么久默认返回0;如果是user.name的话,user为空就返回null,即返回对应类型的默认值。
集合
数组, List, and 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]}"
字符串常量
第一种是让属性值用单引号,字符串本身用双引号。
android:text='@{map["firstName"]}'
然后第二种就是用 `号
android:text="@{map[`firstName`]}"
可以用 + 号连结字符串
android:text="@{`年龄`+user.age}"
资源
在表达式中可以像这样访问资源:
android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
支持字符串的格式化,比如
<string name="age">年龄:%d</string>
android:text="@{@string/age(user.age)}
一些资源要显式声明类型,如下表
Java类型 | 资源类型 | 表达式中类型 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
color int | @color | @color |
ColorStateList | @color | @colorStateList |
Include
变量可以被传递进include内部,用 命名空间:变量名字这样的属性
<?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>