在项目中使用到了 Data Binding,总结使用经验后写成本文。
本文涉及安卓自带框架 DataBinding 的基础使用方法,适合初次接触 Data Binding 的同学阅读。
DataBinding 出现以前,我们在实现 UI 界面时,不可避免的编写大量的毫无营养的代码:比如 View.findViewById()
;比如各种更新 View 属性的 setter:setText()
,setVisibility()
,setEnabled()
或者 setOnClickListener()
等等。
这些“垃圾代码”数量越多,越容易滋生 bug。
使用 DataBinding,我们可以避免书写这些“垃圾代码”。
使用 Data Binding 会增加编译出的 apk 文件的类数量和方法数量。
新建一个空的工程,统计打开 build.gradle 中 Data Binding 开关前后的 apk 文件中类数量和方法数量,类增加了 120+,方法数增加了 9k+(开启混淆后该数量减少为 3k+)。
如果工程对方法数量很敏感的话,请慎重使用 Data Binding。
Gradle 1.5 alpha 及以上自带支持 DataBinding,仅需在使用 DataBinding 的 module 里面的 build.gradle 里面加上配置即可:
android {
...
dataBinding {
enabled = true
}
...
}
在详细学习 DataBinding 之前,我们可以先来看下一个简单的例子:LoginDemo4DataBinding。该例子使用非 DataBinding 技术和 DataBinding 技术 2 种方式,实现了一个简单的登录页面。通过该例子,我们可以直观的感受下 DataBinding 的不同。
下面我们详细讨论 DataBinding 的使用方法,下面的例子实现的是在界面上显示两个文本:姓氏和名字。
公用的 Activity 如下:
public class DataBindingActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TestDbBinding binding = DataBindingUtil.setContentView(this, R.layout.test_db_layout);
// viewmodel
UserViewModel viewModel = new UserViewModel();
binding.setUser(viewModel);
}
}
其中,TestDbBinding 是根据 R.layout.test_db_layout 自动生成的,binding.setUser() 方法也是根据 layout 中 variable name 自动生成的。
公用的 R.layout.test_db_layout 如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="userViewModel" type="com.example.UserViewModel"/>
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="@{userViewModel.firstName}"/>
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userViewModel.lastName}"/>
LinearLayout>
layout>
做过 J2EE 开发的同学,有没有似曾相识感觉?感人感觉,不管是 Data Binding,还是 React Native,都是将 Web 开发的先进思想或技术引进到移动开发领域的一种尝试。更具体的说,是将声明式编程(Declarative programming,如 JavaScript,CSS 等)引入命令式编程(Imperative programming,如 Java 等)中。
<div id="welcome">Welcome <a href="#">{user.username}a> <a href="{site}home/logout">logouta>div>
<div class="clear">div>
{if:isset(menus)}
{if:menus}
<div id="moduleList">
<ul>
{foreach:menus,$menu}
<li {if:$menu.is_active} class="current" {end}><div><a href="{site}{$menu.m_uri}/">{__($menu.m_label)}a>div>li>
{end}
ul>
div>
如果 UI 比较简单,界面仅仅是静态展示,不涉及 UI 的动态更新,以下代码就能满足需求了。
public class UserViewModel {
public final String firstName;
public final String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
即使在复杂的 UI 界面中,多数界面元素仅是静态展示,只有少数界面元素才需要根据用户的操作动态更新。
以微信朋友圈界面为例,一条状态的用户头像、昵称、内容、时间等这些元素一旦加载成功,就不再改变了;而点赞列表和评论列表是动态改变的。
如何使用 DataBinding 动态更新界面数据呢?
有3种方式实现动态更新界面数据:
由于 Java 不允许多继承,而允许同时实现多个接口,所以该方法更具有通用性。
public class UserViewModel extends BaseObservable {
private String firstName;
private String lastName;
public UserViewModel(String firstname, String lastname) {
this.firstName = firstname;
this.lastName = lastname;
}
@Bindable
public String getFirstName() {
return firstName;
}
@Bindable
public String getLastName() {
return lastName;
}
public void setFirstName(String firstname) {
this.firstName = firstname;
notifyPropertyChanged(BR.firstName);
}
public void setLastName(String lastname) {
this.lastName = lastname;
notifyPropertyChanged(BR.lastName);
}
}
BR.java 是类似 R.java 的资源文件,是 Binding Resources 的缩写,由框架自动生成。
注意,BR 中的 id 生成的依据是 @Bindable 修饰的方法名 getXXX(),而非方法体的内容。当在 getXXX() 方法前加 @Bindable 之后, BR.java 中就立即生成常量 BR.xXX。
还有另外一种写法:
public @Bindable String firstName;
@Bindable 可以放在 public 之前或之后,但是不能放在 String 之后。
这种方式,框架会自动生成 getFirstName() 方法。注意,此时变量的访问权限必须是 public
。
如果 @Bindable 修饰的变量和 @Bindable 修饰的该变量的 getter 方法同时存在,则 getter 方法失效。
上述两种方式的区别在于,@Bindable 修饰的 get 方法体,不一定是简单的 return xxx
,也可以是复杂的处理过程。例如,界面上显示的是 displayName,而 displayName 是由 firstName 和 lastName 按一定规则加工生成,改变 firstName 或 lastName 均会导致 displayName 对应的 UI 元素更新。这时,我们可以这么写:
// ...
private String displayName;
// ...
public void setFirstName(String firstName) {
this.firstName = firstName;
notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
}
@Bindable
public String getDisplayName() {
return firstName + "." + lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
notifyPropertyChanged(com.mmlovesyy.displaynamedemo.BR.displayName);
}
和
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{user.displayName}' />
而如果使用 public @Bindable String displayName;
,由于 get 方法是框架自动生成的,方法体是 return displayName;
, 我们将无法做到这种效果。
其实,从这个案例我们可以一窥其动态更新的原理:通过 setLastName() 改变 lastName,并在该方法中通知订阅者,订阅者再调用 getDisplayName() 方法来代替 layout 文件中的 user.displayName,
public class UserViewModel {
public final ObservableField firstName = new ObservableField<>();
public final ObservableField lastName = new ObservableField<>();
public final ObservableInt age = new ObservableInt();
}
ObservableByte / ObservableChar / ObservableShort / ObservableInt /ObservableLong / ObservableFloat / ObservableDouble / ObservableBoolean / ObservableParcelable 为基本数据类型;ObservableField
注意,firstName 的操作方法是 get()
和 set()
方法,例如要更新 firstName 的值:
firstName.set("linus chen");
age.set(age.get() + 1);
推而广之,不管是 ObservableInt/ObservableBoolean/ObservableFloat 等基本数据类型,还是ObservableField
ObservableInt.java
public class ObservableInt extends BaseObservable implements Parcelable, Serializable {
/**
* Set the stored value.
*/
public void set(int value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
}
ObservableField.java
public class ObservableField<T> extends BaseObservable implements Serializable {
/**
* Set the stored value.
*/
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
}
前几节写的都是比较简单的使用方法,是在 View 已经确定的情况下更新其属性(数据、可见性等)。那么如何更新 ViewGroup 呢,即动态的向 ViewGroup 添加或移除子 View 呢?这是我们需要使用 @BindingAdapter({“bind:attr1”, “bind:attr2”}) 注解。
先简要介绍一下该注解的使用方法,然后用一个例子来具体说明。
在 layout 中,使用 “app:attr1” 的格式来添加参数,这些参数会被传递到 @BindingAdapter 修饰的方法中,方法必须是 public static void
类型。注意其中的 static
,这就限制了方法体中使用的变量(基本类型,引用类型,各种 XXXListener等)和方法要么通过 @BindingAdapter 传进去的,要么是 static的。
当你被 static 困扰时,请考虑一下通过 @BindingAdapter 里面的参数传进去。
举个动态生成的 View 的 click 事件和 layout 中 DataBinding 的事件的交互的例子,就是通过参数将 OnClickListener 的实例以参数的形式传给 static 方法,而实例 onClick() 实际调用 ViewModel 中的 handleOnClick() 方法。
举个例子,效果图如下,向一个 LinearLayout 中动态添加 TextView:
layout 代码如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.mmlovesyy.bindingadapterdemo.NamesViewModel" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:id="@+id/add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{viewModel.onClick}"
android:text="+" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:context="@{viewModel.context}"
app:names="@{viewModel.names}">LinearLayout>
LinearLayout>
layout>
Activity 代码如下:
public class MainActivity extends AppCompatActivity {
private NamesViewModel viewModel = new NamesViewModel(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setViewModel(viewModel);
}
}
NamesViewModel 代码如下:
public class NamesViewModel {
public Context context;
public final ObservableArrayList names = new ObservableArrayList<>();
public NamesViewModel(Context context) {
names.add("linus chen");
names.add("lin xueyan");
names.add("zhang xiaona");
names.add("chen lei");
names.add("liu yuhong");
this.context = context;
}
@BindingAdapter({"bind:names", "bind:context"})
public static void setNames(ViewGroup linearLayout, ArrayList names, Context context) {
linearLayout.removeAllViews();
for (String s : names) {
TextView t = new TextView(context);
t.setText(s);
linearLayout.addView(t);
}
}
public void onClick(View v) {
int id = v.getId();
if (id == R.id.add_btn) {
names.add("yanyu cai");
}
}
}
如果编译出错,log 日志的报错信息,即出错的代码行是指框架根据 xml 文件生成的 xxxBinding.java,,由于目前 Android Studio 尚不支持自动定位出错代码行,所以我们要手动去找该文件。xxxBinding.java 文件位置:将 AS 切换成 Project 视图 - 对应的 module(如 app)- build - intermediates - classes - debug - 对应的 package。
然后根据出错代码行,推测对应的 xml 文件中出错的位置。
希望以后 Android Studio 能改善这方面的体验。其实,这也间接要求我们不要在表达式使用复杂的逻辑,越简单越容易调试。
请参考这篇文章:《Android Data Binding从抵触到爱不释手》。
请参考 Marshmallow Brings Data Bindings to Android 视频下方文字部分,在 Performance 一节中,Data Binding 的作者详细论述了其性能。
这里要讲的单元测试主要是针对 @BindingAdapter 修饰的方法的。
我们可以这么写:
public class MyBindingAdapters {
@BindingAdapter("android:text")
public static void setText(TextView view, String value) {
if (isTesting) {
doTestingStuff(view, value);
} else {
TextViewBindingAdapter.setText(view, value);
}
}
}
这有点恶心,而且由于 setText() 方法是 static 的,所以它里面使用的变量或方法都必须是 static 的,即变量 isTesting
和 doTestingStuff()
都是 static 的,更加不方便,这显然是面向过程编程的方法,不符合 OCP 原则(对扩展开放,非修改封闭)。
我们有一种更好的方法来做单元测试。不过首先我们要先来了解 android.databinding.DataBindingComponent
这个接口的用法,弄懂了它的用法,就知道怎么做单元测试。而且不仅仅可以做单元测试,还有其他用途。
UML 图:
DataBindingComponent.java
/**
* This interface is generated during compilation to contain getters for all used instance
* BindingAdapters. When a BindingAdapter is an instance method, an instance of the class
* implementing the method must be instantiated. This interface will be generated with a getter
* for each class with the name get* where * is simple class name of the declaring BindingAdapter
* class/interface. Name collisions will be resolved by adding a numeric suffix to the getter.
*
* An instance of this class may also be passed into static or instance BindingAdapters as the
* first parameter.
*
* If using Dagger 2, the developer should extend this interface and annotate the extended interface
* as a Component.
*
* @see DataBindingUtil#setDefaultComponent(DataBindingComponent)
* @see DataBindingUtil#inflate(LayoutInflater, int, ViewGroup, boolean, DataBindingComponent)
* @see DataBindingUtil#bind(View, DataBindingComponent)
*/
public interface DataBindingComponent {
}
这是一个空的接口,没有声明任何方法。注意文件开头的注释部分。定义一个抽象类,抽象 @BindingAdapter 修饰的方法,这里我们使用 setText() 方法作为例子。
MyBindingAdapter.java
public abstract class MyBindingAdapters {
@BindingAdapter("android:text")
public abstract void setText(MyDataBindingComponent component, TextView view, String value);
}
再定义两个 MyBindingAdapters 的子类,分别用于单元测试和实际生产环境:TestBindingAdapters.java 和 ProdBindingAdapters.java:
TestBindingAdapters.java
private static final String TAG = "TestBindingAdapters";
@Override
public void setText(MyDataBindingComponent component, TextView view, String value) {
// test code
Log.d("TestBindingAdapters", "SETTEXT INVOKED");
}
ProdBindingAdapters.java
public class ProdCBindingAdapters extends MyBindingAdapters {
@Override
public void setText(MyDataBindingComponent component, TextView view, String value) {
TextViewBindingAdapter.setText(view, value);
}
}
注意,这两个类提供的 @BindingAdapter 修饰的方法都是非 static 的。
我们在
DataBindingUtil.setContentView(Activity activity, int layoutId,DataBindingComponent bindingComponent);
中要使用的是 DataBindingComponent,所以我们还要定义一个 MyDataBindingComponent,及其两个子类:TestComponent 和 ProdComponent,分别与 TestBindingAdapters 和 ProdBindingAdapters 相对应。
MyDataBindingComponent.java
public interface MyDataBindingComponent extends android.databinding.DataBindingComponent {
MyBindingAdapters getMyBindingAdapters();
}
TestComponent.java
public class TestComponent implements MyDataBindingComponent {
private MyBindingAdapters mAdapter = new TestBindingAdapters();
@Override
public MyBindingAdapters getMyBindingAdapters() {
return mAdapter;
}
}
ProdComponent.java
public class ProdComponent implements MyDataBindingComponent {
private String color;
public ProdComponent(String _color) {
color = _color;
}
private MyBindingAdapters mAdapter = new ProdCBindingAdapters();
@Override
public MyBindingAdapters getMyBindingAdapters() {
return mAdapter;
}
public String getColor() {
return color;
}
}
然后在 MainActivity 中调用:
public class MainActivity extends AppCompatActivity {
private UserViewModel user;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
user = new UserViewModel("linus", "chen");
binding.setUser(user);
}
@BindingAdapter("android:url")
public static void setColor(ProdComponent component, TextView view, String url) {
view.setText(component.getColor());
}
}
注意,@BindingAdapter 修饰的方法包含在类 MyBindingAdapter 中,所以 DataBindingUtil.setContentView(this, R.layout.activity_main, new ProdComponent("blue"));
中的最后一个参数,DataBindingComponent 类中必须包含一个名为 getMyBindingAdapter() 的 getter 方法,遵循本节开头的文件注释中的规定。
这样,我们使用 DataBindingUtil.setContentView(this, R.layout.activity_main, new TestComponent());
进行单元测试。
我们还可以看到,DataBindingComponent 中可以书写其他方法(例如网络下载方法等),供 @BindingAdapter 修饰的方法(不论是 static 或非 static)调用。
如果要使用 Dagger2, 则代码如下:
Dagger2
@Module
public class TestModule {
@Provides
public MyBindingAdapters getMyBindingAdapter() {
return TestBindingAdapter();
}
}
@Component(modlues = TestModule.class)
public interface TestComponent extends android.databinding.DataBindingComponent {
}
DataBindingUtil.setDefaultComponent(DaggerTestComponent.create());
还有一点需要注意的是,无自定义 DataBindingComponent 时框架生成的 ActivityMainBinding.java 相关代码:
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, firstNameUser);
自定义 DataBindingComponent 时生成的相关代码:
this.mBindingComponent.getMyBindingAdapters().setText(this.mboundView2, firstNameUser);
对比可以看出,当自定义 DataBindingAdapter 时,框架会自动调用自定义的 setText() 方法,而非默认的 TextViewBindingAdapter.setText()。
至于 DataBinding 怎么写才是最好的,可以参考 Google 的这个开源项目:Android Architecture Blueprints [beta],使用 DataBinding、MVP、DataBinding+MVP 等多种方式实现同一个便笺应用,包括其中的单元测试的写法,都非常值得学习。
xml 文件中不要出现业务逻辑,只出现简单的 UI 相关的表达式;
在 xml 绑定变量时尽量使用 ” 代替 “”,即使如此,转义依然不可避免:
- ‘&’ –> ‘&;(用英文分号替换)’;
- ‘<’ –> ‘<;(用英文分号替换)’;
- ‘>’ -> ‘>;(用英文分号替换)’;
有时会遇到莫名其妙的检查错误,可尝试 clean 工程,或无视之;
关于代码结构,使用 ViewModel(如 UserViewModel)+ Model(如 UserModel) + xml 的结构,对点击事件的处理以及 View 的状态数据(如评论列表是否展开,当前用户登录信息等)放在 ViewModel 中,而正常的数据放在 Model 中。
DataBinding 和 findViewById() 并不是互斥的,即使在使用 DataBinding 的工程中,我们仍然可以根据需要使用之,特别是在动态更新 ViewGroup 的情景中,有时不可避免的要是用该方法。
DataBinding 已经对 null 做了处理,我们无需再关心表达式 npe 的问题,例如 binding.setUser(viewModel);
中 viewModel 为 null 时,运行时不会出现 npe。
关于 @BindingMethod
这个注解是用来关联 SDK 中提供的控件的属性和方法的,这些属性的名称和其 setter 不匹配,需要 @BindingMethod 来“牵绳拉线”,以便自动更新属性的时候调用其对应的 setter。具体的使用方法可以参考 android.databinding.adapters.ImageViewBindingAdapter.java。
开发者一般用不到该注解。
也许本文不值得一看,但是下面这些资料则不然。