Android Data Binding从抵触到爱不释手

1 引入

如何高效地实现以下界面?

登录/未登录

有好几年findViewById实战经验的我,感觉并不难啊。一般会

  • 1.先定义一个User的Model类,数据来自JSON解析;
  • 2.创建一个xml,随后在xml中布局完所有View,对头像、标题、积分、登录按钮一个id;
  • 3.在Activity中通过findViewById获取到头像ImageView、标题TextView、积分TextView、登录Button,然后给Button设置监听器,再根据登陆状态展示对应数据;

实现如下:

  • User.java
public class User {
    private String name;
    private int score;
    private int level;
    private int avatar;

    public String getName() { return name; }

    public void setName(String name) { this.name = name; }

    public int getScore() { return score; }

    public void setScore(int score) { this.score = score; }

    public int getLevel() { return level; }

    public int getAvatar() { return avatar; }

    public void setAvatar(int avatar) { this.avatar = avatar; }

    public void setLevel(int level) { this.level = level; }

    public static User newInstance() {
        User user = new User();
        user.setName("王大锤:" + (int)(Math.random() * 10));
        user.setScore((int) (Math.random() * 999));
        user.setLevel((int) (Math.random() * 77));
        user.setAvatar(R.drawable.avatar);
        return user;
    }
}
  • activity_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <View
        android:background="@color/detail_background"
        android:layout_width="match_parent"
        android:layout_height="66dp">
    </View>
    <ImageView
        android:id="@+id/detail_avatar"
        android:layout_gravity="center"
        android:src="@drawable/avatar"
        android:layout_marginTop="-33dp"
        android:layout_width="66dp"
        android:layout_height="66dp" />
    <TextView
        android:id="@+id/detail_name"
        android:textSize="17sp"
        android:textColor="@color/textColorPrimary"
        android:layout_marginTop="15dp"
        android:layout_gravity="center"
        android:text="王大锤"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/detail_desc"
        android:layout_marginTop="15dp"
        android:textSize="13sp"
        android:layout_gravity="center"
        android:text="积分:102 金币:0"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/detail_action_button"
        android:layout_marginTop="15dp"
        android:layout_gravity="center"
        android:text="退出登陆"
        android:textColor="@color/white"
        android:background="@drawable/selector_g_button"
        android:layout_width="220dp"
        android:layout_height="wrap_content" />
</LinearLayout>
  • DetailActivity
public class DetailActivity extends AppCompatActivity {

    ImageView avatarIV;
    TextView nameTV;
    TextView descTV;
    Button actionBtn;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);

        initView();
        login();
    }

    private void login(){ fill(User.newInstance()); }

    private void logout(){ fill(null); }

    private void initView() {
        avatarIV = (ImageView) findViewById(R.id.detail_avatar);
        nameTV = (TextView) findViewById(R.id.detail_name);
        descTV = (TextView) findViewById(R.id.detail_desc);
        actionBtn = (Button) findViewById(R.id.detail_action_button);
    }

    private void fill(final User user){
        final int visibility = user != null ? View.VISIBLE : View.GONE;
        if (avatarIV != null){
            avatarIV.setVisibility(visibility);
            if (user != null)
                avatarIV.setImageDrawable(ContextCompat.getDrawable(this,user.getAvatar()));
        }

        if (nameTV != null){
            nameTV.setVisibility(visibility);
            if (user != null)
                nameTV.setText(user.getName());
        }

        if (descTV != null){
            descTV.setVisibility(visibility);
            if (user != null)
                descTV.setText(String.format("积分:%d 等级:%d",user.getScore(),user.getLevel()));
        }

        if (actionBtn != null){
            actionBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (user == null) login();
                    else logout();
                }
            });
            actionBtn.setText(user == null ? "登录":"退出登录");
        }
    }
}

2 去掉烦人的findViewById(View注入)

可以看到,在Activity中View的定义、find、判空占据了大量篇幅,我们需要更优雅的实现。

2.1 ButterKnife

你可能听说过Jake Wharton的ButterKnife,这个库只需要在定义View变量的时候通过注解传入对应id,随后在onCreate时调用ButterKnife.bind(this)即可完成view的注入,示例如下:

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @Override public void onCreate(Bundle savedInstanceState{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
  }
}

2.2 Android Data Binding

如果使用了Android Data Binding,那么View的定义、find、判空这些都不用写了,如何做呢?

2.2.1 准备工作

首先,你需要满足一个条件:你的Android Plugin for Gradle版本必须等于或高于1.5.0-alpha1版本,这个版本位于根目录build.gradle中,示例如下:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0-rc1'
    }
}

接着,你必须告诉编译器开启Data Binding,一般位于app:build.gradle的android标签中,示例如下:

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    dataBinding {
        enabled true
    }
    ...
}

2.2.2 修改layout.xml

activity_detail.xml为例,原来的根节点为LinearLayout,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<!-- LinearLayout为根节点 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <View
        android:background="@color/detail_background"
        android:layout_width="match_parent"
        android:layout_height="66dp">
    </View>
    ....
</LinearLayout>

我们拷一份activity_detail.xml,改为activity_detail2.xml,并且需要在外面wrap一层layout标签,修改后的activity_detail2.xml为:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- LinearLayout为原布局根节点 -->
    <LinearLayout
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <View
            android:background="@color/detail_background"
            android:layout_width="match_parent"
            android:layout_height="66dp">
        </View>
        ...
    </LinearLayout>
</layout>

2.2.3 开始享受乐趣吧!

在上述操作完成后,编译器会自动为我们生成
com.asha.demo.databinding.ActivityDetail2Binding.java类,这个类的命令方式为:包名 + databinding + activity_detail2驼峰命名方式 + Binding.java。随后,使用这个activity_detail2DetailActivity2.java的代码可以简化为:

public class DetailActivity2 extends AppCompatActivity {

    ActivityDetail2Binding binding;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this,R.layout.activity_detail2);
        
        login();
    }

    private void login(){ fill(User.newInstance()); }

    private void logout(){ fill(null); }

    private void fill(final User user){
        final int visibility = user != null ? View.VISIBLE : View.GONE;
        if (user != null){
            binding.detailAvatar.setImageDrawable(ContextCompat.getDrawable(this,user.getAvatar()));
            binding.detailName.setText(user.getName());
            binding.detailDesc.setText(String.format("积分:%d 等级:%d",user.getScore(),user.getLevel()));
        }

        binding.detailAvatar.setVisibility(visibility);
        binding.detailName.setVisibility(visibility);
        binding.detailDesc.setVisibility(visibility);
        binding.detailActionButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                if (user == null) login();
                else logout();
            }
        });
        binding.detailActionButton.setText(user == null ? "登录":"退出登录");
    }
}

是的,所有View的定义、find、判空都不见了,所有的这些操作都在编译器为我们生成的ActivityDetail2Binding.java中完成,只需要在onCreate时调用如下代码进行setContentView即可实现,

binding = DataBindingUtil.setContentView(this,R.layout.activity_detail2);
我的天哪

2.2.4 ActivityDetail2Binding中注入View相关的代码分析

可以在as中方便的查看编译器自动生成的类,这个类位于/app/build/intermediates/classes/debug/com/asha/demo/databinding/ActivityDetail2Binding.class中,缩减掉Binding逻辑后的代码为:

public class ActivityDetail2Binding extends ViewDataBinding {
    private static final IncludedLayouts sIncludes = null;
    private static final SparseIntArray sViewsWithIds = new SparseIntArray();
    public final Button detailActionButton;
    public final ImageView detailAvatar;
    public final TextView detailDesc;
    public final TextView detailName;
    private final LinearLayout mboundView0;
    private long mDirtyFlags = -1L;

    public ActivityDetail2Binding(DataBindingComponent bindingComponent, View root) {
        super(bindingComponent, root, 0);
        Object[] bindings = mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds);
        this.detailActionButton = (Button)bindings[4];
        this.detailAvatar = (ImageView)bindings[1];
        this.detailDesc = (TextView)bindings[3];
        this.detailName = (TextView)bindings[2];
        this.mboundView0 = (LinearLayout)bindings[0];
        this.mboundView0.setTag((Object)null);
        this.setRootTag(root);
        this.invalidateAll();
    }
    ...
    static {
        sViewsWithIds.put(2131492948, 1);
        sViewsWithIds.put(2131492949, 2);
        sViewsWithIds.put(2131492950, 3);
        sViewsWithIds.put(2131492951, 4);
    }
}

其中全局静态SparseIntArray数组中存放了4个数字,这个四个数字为R.java中生成的对应View的id,

public final class R {
    ...
    public static final class id {
        ...
        public static final int detail_action_button = 2131492951;
        public static final int detail_avatar = 2131492948;
        public static final int detail_desc = 2131492950;
        public static final int detail_name = 2131492949;
        ...
    }
    ...
}

ActvityDetail2Binding实例构造的时候调用了mapBindings,一次解决了所有View的查找,mapBindings函数在ActvityDetail2Binding父类ViewDataBinding中实现。

3 使用表达式在layout.xml中填充model数据

ActivityDetail2.java中还存在大量的View控制、数据填充代码,如何把这些代码在交给layout.xml完成呢?

3.1 ModelAdapter类

第2节中已经定义了User.java类作为Model类,但是我们经常会遇到Model类和真正View展示不一致的情况,本例子中定义一个来ModelAdapter类来完整Model数据到展示数据的适配。示例代码为ActivityDetail3.java的内部类,可以调用ActivityDetail3.java中的函数,代码定义如下:

public class DetailActivity3 extends AppCompatActivity {
    public class ModelAdapter {
        private User user;

        public ModelAdapter(User user) { this.user = user;}

        public String getName(){ return user != null ? user.getName() : null;}

        public Drawable getAvatar(){
            return user != null ? ContextCompat.getDrawable(DetailActivity3.this,user.getAvatar()) : null;
        }

        public String getDesc(){
            return user != null ? String.format("积分:%d 等级:%d",user.getScore(),user.getLevel()) : null;
        }

        public String actionText(){ return user != null ? "退出登录" : "登陆"; }

        public void clickHandler(View view){
            if (user != null) logout();
            else login();
        }
    }
}

3.2 activity_detail3.xml中使用model

同样复制一份activity_detail2.xmlactivity_detail3.xml,在<layout>节点加入<data>节点,并且在里面定义需要用的model类(比如ModelAdapter adapter),当然也可以是基础类型变量(比如int visibility);

随后,就可以在下面的view中使用表达式了,全部布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="adapter" type="com.asha.demo.DetailActivity3.ModelAdapter"/>
        <variable name="visibility" type="int"/>
    </data>
    <LinearLayout
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
        <View
            android:background="@color/detail_background"
            android:layout_width="match_parent"
            android:layout_height="66dp">
        </View>
        <ImageView
            android:src="@{adapter.avatar}"
            android:visibility="@{visibility}"
            android:id="@+id/detail_avatar"
            android:layout_gravity="center"
            android:layout_marginTop="-33dp"
            android:layout_width="66dp"
            android:layout_height="66dp" />
        <TextView
            android:visibility="@{visibility}"
            android:text="@{adapter.name}"
            android:id="@+id/detail_name"
            android:textSize="17sp"
            android:textColor="@color/textColorPrimary"
            android:layout_marginTop="15dp"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:visibility="@{visibility}"
            android:text="@{adapter.desc}"
            android:id="@+id/detail_desc"
            android:layout_marginTop="15dp"
            android:textSize="13sp"
            android:layout_gravity="center"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <Button
            android:text="@{adapter.actionText}"
            android:onClick="@{adapter.clickHandler}"
            android:id="@+id/detail_action_button"
            android:layout_marginTop="15dp"
            android:layout_gravity="center"
            android:textColor="@color/white"
            android:background="@drawable/selector_g_button"
            android:layout_width="220dp"
            android:layout_height="wrap_content" />
    </LinearLayout>
</layout>

3.3 DetailActivity3.java中调用填充

如下代码所示,只需要在登录状态改变的时候,给viewDataBinding设置所需要的adatper、visibility值,即可完成数据的填充

public class DetailActivity3 extends AppCompatActivity {

    ActivityDetail3Binding binding;
    public class ModelAdapter {
        ...
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this,R.layout.activity_detail3);
        login();
    }

    private void login(){
        fill(User.newInstance());
    }

    private void logout(){
        fill(null);
    }

    private void fill(final User user){
        binding.setAdapter(new ModelAdapter(user));
        binding.setVisibility( user != null ? View.VISIBLE : View.GONE);
    }
}

3.4 ActivityDetail3Binding中填充相关的代码分析

同样,ActivityDetail3Binding中,编译器根据activity_detail3.xml中的<data>标签,自动生成了诸如setAdapter、setVisibility的代码,setAdapter相关代码如下:

public class ActivityDetail3Binding extends ViewDataBinding{
    
    private ModelAdapter mAdapter;
    ...
    public void setAdapter(ModelAdapter adapter) {
        this.mAdapter = adapter;
        synchronized(this) {
            this.mDirtyFlags |= 1L;
        }

        this.notifyPropertyChanged(1);
        super.requestRebind();
    }

    public ModelAdapter getAdapter() {
        return this.mAdapter;
    }
    ...
}

非常简单,自动生成了getter和setter,在完成set操作后,调用执行notifyPropertyChangedsuper.requestRebind()

  • notifyPropertyChanged
    ViewDataBinding本身就是一个BaseObservable, 在往ViewDataBinding注册观察某个属性的变化,如果注册了mAdapter的变化,对应的观察器就会接收到回调。相关逻辑与反向Binding相关,谷歌官方还没给出相关使用文档,不再深入分析;

  • super.requestRebind()
    1.此函数为ViewDataBinding中的函数,具体实现为判断现在是否有Rebind请求,如果有则return;如果没有则根据运行时sdk版本交给handler或者choreographer插入到下一帧中执行mRebindRunnable。
    2.在mRebindRunnable中会根据当前sdk版本,如果大于等于KITKAT,则需要在onAttachToWindow后执行executePendingBindings;否则直接执行executePendingBindings。

public abstract class ViewDataBinding extends BaseObservable {
    protected void requestRebind() {
        synchronized (this) {
            if (mPendingRebind) {
                return;
            }
            mPendingRebind = true;
        }
        if (USE_CHOREOGRAPHER) {
            mChoreographer.postFrameCallback(mFrameCallback);
        } else {
            mUIThreadHandler.post(mRebindRunnable);
        }

    }

    /**
     * Runnable executed on animation heartbeat to rebind the dirty Views.
     */
    private final Runnable mRebindRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (this) {
                mPendingRebind = false;
            }
            if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
                // Nested so that we don't get a lint warning in IntelliJ
                if (!mRoot.isAttachedToWindow()) {
                    // Don't execute the pending bindings until the View
                    // is attached again.
                    mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                    mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                    return;
                }
            }
            executePendingBindings();
        }
    };
}

3.在父类ViewDataBinding中经过一些的判断,调用到ActivityDetail3Binding中的executeBindings,在executeBindings中根据dirtyFlags执行不同的View属性赋值,以下所有ActivityDetail3Binding相关代码都是编译器自动生成的

public class ActivityDetail3Binding extends ViewDataBinding{
    ...

    protected void executeBindings() {
        long dirtyFlags = 0L;
        synchronized(this) {
            dirtyFlags = this.mDirtyFlags;
            this.mDirtyFlags = 0L;
        }

        Drawable avatarAdapter = null;
        ModelAdapter adapter = this.mAdapter;
        String descAdapter = null;
        String nameAdapter = null;
        ActivityDetail3Binding.OnClickListenerImpl androidViewViewOnCli = null;
        String actionTextAdapter = null;
        int visibility = this.mVisibility;
        if((dirtyFlags & 5L) != 0L && adapter != null) {
            avatarAdapter = adapter.getAvatar();
            descAdapter = adapter.getDesc();
            nameAdapter = adapter.getName();
            androidViewViewOnCli = (this.mAndroidViewViewOnCl == null?(this.mAndroidViewViewOnCl = new ActivityDetail3Binding.OnClickListenerImpl()):this.mAndroidViewViewOnCl).setValue(adapter);
            actionTextAdapter = adapter.actionText();
        }

        if((dirtyFlags & 6L) != 0L) {
            ;
        }

        if((dirtyFlags & 5L) != 0L) {
            TextViewBindingAdapter.setText(this.detailActionButton, actionTextAdapter);
            this.detailActionButton.setOnClickListener(androidViewViewOnCli);
            ImageViewBindingAdapter.setImageDrawable(this.detailAvatar, avatarAdapter);
            TextViewBindingAdapter.setText(this.detailDesc, descAdapter);
            TextViewBindingAdapter.setText(this.detailName, nameAdapter);
        }

        if((dirtyFlags & 6L) != 0L) {
            this.detailAvatar.setVisibility(visibility);
            this.detailDesc.setVisibility(visibility);
            this.detailName.setVisibility(visibility);
        }

    }
    ...
}

至此,完成了View数据的填充分析。

4 Binding

自动生成的ViewDataBinding类(例如ActivityDetail3Binding)内包含了Model + View,是MVVM中的MV的概念。

第2章的View注入,第3章的View赋值都是铺垫,他们最后都是为Binding操作进行服务。目前谷歌已经支持双向Binding,但上文已经提到,目前资料比较少。本文只关注单向的Binding,即:Model的变化,自动同步到View上。

4.1 使用ObservableField

目前所提供的ObservableField有:

Observable类型 对应原类型
ObservableArrayList ArrayList
ObservableArrayMap ArrayMap
ObservableBoolean boolean
ObservableByte byte
ObservableChar char
ObservableFloat float
ObservableDouble double
ObservableLong long
ObservableInt int
ObservableParcelable<T extends Parcelable> <T extends Parcelable>
ObservableField<T> <T>

本文使用简单的ObservableInt作为示例,解决visibility的单项绑定问题。

  • 改造activity_detail4.xml:定义类型为ObservableInt的variable,name为visibility,随后赋值给ImageView的android:visibility,示例如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="visibility" type="android.databinding.ObservableInt"/>
    </data>
    <LinearLayout
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
       ...
        <ImageView
            android:visibility="@{visibility.get()}"
            android:id="@+id/detail_avatar"
            android:layout_gravity="center"
            android:layout_marginTop="-33dp"
            android:layout_width="66dp"
            android:layout_height="66dp" />
        ...
    </LinearLayout>
</layout>
  • 改造DetailActivity4.java,只需要在onCreate时把visibility赋值给binding(ActivityDetail4Binding)即可,后面对visibility的操作,就会更新到view上,示例代码如下:
public class DetailActivity4 extends AppCompatActivity {
    ActivityDetail4Binding binding;
    ObservableInt visibility = new ObservableInt();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this,R.layout.activity_detail4,new MyComponent());
        binding.setVisibility(visibility);
        login();
    }

    private void login(){  fill(User.newInstance());  }

    private void logout(){ fill(null); }

    private void fill(final User user){
        visibility.set(user != null ? View.VISIBLE : View.GONE);
        ....
    }
    ....
}

4.2 ActivityDetail4Binding中单向绑定相关的代码分析

与给ActivityDetail4Binding直接set纯Model不同,所有的ObservableField都实现了Observable接口,只要实现了Observable接口,都是单向Binding类型,所以ActivityDetail4Binding中的setVisibility多加了一行代码:this.updateRegistration(1, visibility),其中1为propertyId,目前一共自动生成了2个,0为adatper,1为visibility,代码如下:

public class ActivityDetail4Binding extends ViewDataBinding {
    ...
    public void setVisibility(ObservableInt visibility) {
        this.updateRegistration(1, visibility);
        this.mVisibility = visibility;
        synchronized(this) {
            this.mDirtyFlags |= 2L;
        }

        this.notifyPropertyChanged(3);
        super.requestRebind();
    }
    ...
}

updateRegistration函数为ViewDataBinding中的函数,会根据 Observable、ObservableList、ObservableMap三种类型,分别创建对应的Listener。ObservableInt为Observable,所以会使用CREATE_PROPERTY_LISTENER,在registerTo函数中创建WeakPropertyListener
代码如下:

public abstract class ViewDataBinding extends BaseObservable {
    ...
    private boolean updateRegistration(int localFieldId, Object observable,
            CreateWeakListener listenerCreator) {
        if (observable == null) {
            return unregisterFrom(localFieldId);
        }
        WeakListener listener = mLocalFieldObservers[localFieldId];
        if (listener == null) {
            registerTo(localFieldId, observable, listenerCreator);
            return true;
        }
        if (listener.getTarget() == observable) {
            return false;//nothing to do, same object
        }
        unregisterFrom(localFieldId);
        registerTo(localFieldId, observable, listenerCreator);
        return true;
    }
    ...
}

在WeakPropertyListener的mListener有个setTarget函数,这个函数会向mObservable(即外面传进来的visibility)注册一个监听器,如果visibility值发生变化,这个listener就会得到通知,回调到WeakPropertyListeneronPropertyChanged,接着通知到binding(ActivityDetail4Binding)的handleFieldChange,在handleFieldChange中调用了ActivityDetail4BindingonFieldChange函数,如果返回值为true,则在handleFieldChange中调用requestRebind(),通知View进行赋值更新界面,onFieldChange相关代码如下:

public abstract class ViewDataBinding extends BaseObservable {
    ...
   private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
        boolean result = onFieldChange(mLocalFieldId, object, fieldId);
        if (result) {
            requestRebind();
        }
    }
    ...
}
public class ActivityDetail4Binding extends ViewDataBinding {
    ...
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch(localFieldId) {
        case 0:
            return this.onChangeAdapter((ModelAdapter)object, fieldId);
        case 1:
            return this.onChangeVisibility((ObservableInt)object, fieldId);
        default:
            return false;
        }
    }
    ...
}

4.3 Observable Objects

与4.1 ObservableField类似,可以改造一下ModelAdapter:为getter方法增加@Bindable注解,为setter方法增加notifyPropertyChanged(com.asha.demo.BR.name)通知。其中,BR是根据@Bindalbe自动生成的类,给getter方法增加@Bindable注解后,BR文件自动会生成一个整型的name。改造后代码如下:

public class DetailActivity4 extends AppCompatActivity {

    ActivityDetail4Binding binding;
    ObservableInt visibility = new ObservableInt();

    public class ModelAdapter extends BaseObservable{
        private User user;

        public ModelAdapter(User user) {
            this.user = user;
        }
        ...
        @Bindable
        public String getName(){
            return user != null ? user.getName() : null;
        }

        public void setName(String name){
            if (user != null) user.setName(name);
            notifyPropertyChanged(com.asha.demo.BR.name);
        }
        ...
    }
    ...
}

随后,在DetailActivity4.java中调用测试代码,执行完会在1秒后改变adapter上的name值,并且同步到View上,测试代码如下:

binding.detailActionButton.postDelayed(new Runnable() {
    @Override
    public void run() {
        adapter.setName("haha");
    }
},1000);

具体原理与4.1类似,不再赘述。

5 layout.xml中View属性的setter

在下述示例中,detail_name这个TextView想把adapter.name赋值给自身的text属性,就需要调用textView.setText(String)方法,这个方法就是View属性的setter方法。

<TextView
    android:text="@{adapter.name}"
    android:id="@+id/detail_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

5.1 @BindingAdapter

上述的setter方法,Data Binding库帮我们实现了大部分默认方法,具体方法参见android.databinding.adapters包下的类,下图为ViewBindingAdatper具体实现,

ViewBindingAdatper

其中setter方法都为static方法,第一个参数都为自身的实例,后面为xml中传入的参数,只要加入@BindingAdapter注解,编译器就会全局搜索保存在一个temp文件中,并在生成类似ActivityDetail4Binding过程中去查找所需的setter方法的。如果需要自定义,只需要在任意app代码中定义@BindingAdapter即可,例如:

public class DetailActivity4 extends AppCompatActivity {
    @BindingAdapter("android:alpha")
    public static void globalSetAlpha(View view, float alpha) {
        view.setAlpha(alpha);
    }
}

5.2 DataBindingComponent

很多情况下只是某个Binding文件(例如ActivityDetail4Binding)需要自定义setter方法,这个时候就需要使用DataBindingComponent,

  • 首先,定义一个MyComponent,
public class MyComponent implements android.databinding.DataBindingComponent {
    @BindingAdapter("android:alpha")
    public void setAlpha(View view, float alpha) {
        view.setAlpha(0.5f);
    }

    @Override
    public MyComponent getMyComponent() {
        return new MyComponent();
    }
}
  • 接着,在生成Binding对象时传入这个DataBindingComponent实例,代码如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this,R.layout.activity_detail4,new MyComponent());
    ...
}

完成后,这个ActivityDetail4Binding范围内的所有android:alpha="@{foo}"的方式赋值alpha的setter函数都会使用MyComponent#setAlpha

5.3 @BindingConversion

有时候会遇到类型不匹配的问题,比如R.color.white是int,但是通过Data Binding赋值给android:background属性后,需要把int转换为ColorDrawable,实现方式如下:

  • 1.定义一个静态函数,放在项目任意类中,
@BindingConversion
public static Drawable convertColorToDrawable(int drawable) {
    return new ColorDrawable(drawable);
}
  • 2.在layout.xml中使用Data Binding,如:
<View
android:background="@{adapter.avatar != null ? @color/detail_background : @color/colorAccent }"
android:layout_width="match_parent"
android:layout_height="66dp">

对应在ActivityDetail4Binding.java中生成的代码如下所示,其中AvatarAdapterObjectn1为int类型:

ViewBindingAdapter.setBackground(this.mboundView1, DetailActivity4.convertColorToDrawable(AvatarAdapterObjectn1));

5.4 @BindingMethod

例如layout.xml中android:onClick属性,在Binding中真正使用setter时,就对应到了setOnClickListener方法,

@BindingMethod(type = View.class, attribute = "android:onClick", method = "setOnClickListener"),

6 Data Binding利用编译器在背后做的那些事儿

Data Binding相关的jar包由四部分组成,

  • 1.baseLibrary-2.1.0-rc1.jar
    作为运行时类库被打进APK中;

  • 2.DataBinderPlugin(gradle plugin)
    在编译期使用,利用gradle-api(之前叫transform-api,1.5生,2.0改名)处理xml文件,生成DataBindingInfo.java;

  • 3.compiler-2.1.0-rc1.jar
    在编译器使用,入口类继承自AbstractProcessor,用于处理注解,并生成Binding类,DataBindingCompoent.java,DataBinderMapper.java类;

  • 4.compilerCommon-2.1.0-rc1.jar
    DataBinderPlugincompiler-2.1.0-rc1.jar所依赖

为了提高运行时的效率,Data Binding在背后做了非常多的工作,下图是我整理的编译流程,如图所示:

Data Binding编译流程

6.1 相关对象介绍

  • 白色部分为输入,包括
    1.res/layout;
    2.源代码中的注解;

  • 黄色部分为编译器处理类,包括
    1.aapt编译时处理,入口类名为MakeCopy.java;
    2.gradle-api处理,入口类名为DataBinderPlugin.java;
    3.AbstractProcessor处理,入口类名为ProcessDataBinding.java

  • 蓝色部分为中间产物,包括
    1.data-binding-info文件夹,包含了layout的基本信息,导入的变量,View标签中的表达式,标签的位置索引等等,如下所示为data-binding-info/activity_detail3-layout.xml

<?xml version="1.0" encoding="utf-8"?>
<Layout layout="activity_detail3" modulePackage="com.asha.demo" absoluteFilePath="/Users/Asha/Desktop/AndroidDataBinding/app/src/main/res/layout/activity_detail3.xml" directory="layout" isMerge="false">
  
  <Variables declared="true" type="com.asha.demo.DetailActivity3.ModelAdapter" name="adapter">
    <location startLine="3" startOffset="8" endLine="3" endOffset="83"/>
  </Variables>

  <Variables declared="true" type="int" name="visibility">
    <location startLine="4" startOffset="8" endLine="4" endOffset="47"/>
  </Variables>
  <Targets>

    <Target tag="layout/activity_detail3_0" view="LinearLayout">
      <Expressions/>
      <location startLine="6" startOffset="4" endLine="51" endOffset="18"/>
    </Target>

    <Target id="@+id/detail_avatar" tag="binding_1" view="ImageView">
      <Expressions>
        <Expression text="adapter.avatar" attribute="android:src">
          <Location startLine="15" startOffset="12" endLine="15" endOffset="42"/>
          <TwoWay>false</TwoWay>
          <ValueLocation startLine="15" startOffset="27" endLine="15" endOffset="40"/>
        </Expression>

        <Expression text="visibility" attribute="android:visibility">
          <Location startLine="16" startOffset="12" endLine="16" endOffset="45"/>
          <TwoWay>false</TwoWay>
          <ValueLocation startLine="16" startOffset="34" endLine="16" endOffset="43"/>
        </Expression>
      </Expressions>
      <location startLine="14" startOffset="8" endLine="21" endOffset="42"/>
    </Target>

    ....
  </Targets>
</Layout>

2.setter_store.bin,包含所有setter相关信息;
3.layoutinfo.bin,包含所有layout相关信息;
4.br.bin,包含所有BR相关信息;
以上bin文件都以Serializable方式序列化到磁盘上,需要的时候进行反序列化操作;

  • 绿色部分为最终产物,包括
    1.data-binding-layout-out(最终输出到res/layout),即去掉根节点<layout>,去掉节点<data>,与不使用Data Binding时的layout相一致,例如data-binding-layout-out/activity_detail2.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent" android:tag="layout/activity_detail2_0" xmlns:android="http://schemas.android.com/apk/res/android">
    <View
        android:background="@color/detail_background"
        android:layout_width="match_parent"
        android:layout_height="66dp">
    </View>
    ...
</LinearLayout>

2.DataBindingInfo.class,一个看似空的类,但在SOURCE阶段包含了一个@BindingBuildInfo注解,包含了基本DataBinding的基本信息,代码如下:

// DataBindingInfo.class
public class DataBindingInfo {
    public DataBindingInfo() {
    }
}
// @BindingBuildInfo
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface BindingBuildInfo {
    String buildId();
    String modulePackage();
    String sdkRoot();
    int minSdk();
    String layoutInfoDir();
    String exportClassListTo();
    boolean isLibrary();
    boolean enableDebugLogs() default false;
    boolean printEncodedError() default false;
}

3.DataBindingComponent.class,会根据自定义的DataBindingComponent自动生成对应实例化方法,例如:

public interface DataBindingComponent {
    MyComponent getMyComponent();
}

4.ViewDataBinding.class的子类(ActivityDetail2Binding.class等)
5.BR.class,Bindable属性索引表,例如:

public class BR {
    public static final int _all = 0;
    public static final int adapter = 1;
    public static final int name = 2;
    public static final int visibility = 3;

    public BR() {
    }
}

6.DataBindingMapper.class,Mapper,用于寻找某个layout.xml对应的ViewDataBinding类,例如:

class DataBinderMapper {
    static final int TARGET_MIN_SDK = 16;

    public DataBinderMapper() {
    }

    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view, int layoutId) {
        switch(layoutId) {
        case 2130968602:
            return ActivityDetail2Binding.bind(view, bindingComponent);
        case 2130968603:
            return ActivityDetail3Binding.bind(view, bindingComponent);
       ....
        default:
            return null;
        }
    }

    ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View[] views, int layoutId) {
        return null;
    }

    int getLayoutId(String tag) {
        if(tag == null) {
            return 0;
        } else {
            int code = tag.hashCode();
            switch(code) {
            case -600937657:
                if(tag.equals("layout/activity_detail2_0")) {
                    return 2130968602;
                }
                break;
            case -600936696:
                if(tag.equals("layout/activity_detail3_0")) {
                    return 2130968603;
                }
                break;
            ....

            return 0;
        }
    }

    String convertBrIdToString(int id) {
        return id >= 0 && id < DataBinderMapper.InnerBrLookup.sKeys.length?DataBinderMapper.InnerBrLookup.sKeys[id]:null;
    }

    private static class InnerBrLookup {
        static String[] sKeys = new String[]{"_all", "adapter", "name", "visibility"};

        private InnerBrLookup() {
        }
    }
}

6.2 相关编译流程

  • STEP1 资源处理
    aapt或者gradle执行时,都会触发资源处理,在资源处理过程中,DataBinding都会扫描一遍现有的资源,生成不包含<layout>data-binding-layout-out以及DataBinding所需要的data-binding-info
  • STEP2 DataBindingInfo.class生成
    在完成资源处理后,aapt或者gradle-api都会去执行DataBindingInfo.class生成操作,把相关的信息写入DataBindingInfo.class@BindingBuildInfo注解中;
  • STEP3 监听到注解变化
    生成@BindingBuildInfo注解,或者code中发现有新的注解写入,AbstractProcessor注解处理器就开始执行注解处理。DataBinding中有一个ProcessDataBinding.java类专门来处理DataBinding相关的注解;
  • STEP4 ProcessDataBinding处理注解,生成bin
    ProcessDataBinding中处理注解永远会按顺执行3步,ProcessMethodAdapterProcessExpressionsProcessBindable。每次执行都会从磁盘反序列化对应的bin文件,然后忘bin中写入新的,完成后再序列化到磁盘;
  • STEP5 生成最终产物
    执行ProcessMethodAdapter生成DataBindingComponents.class;执行ProcessExpressions生成ViewDataBinding.class子类(ActivityDetail2Binding.class),并触发DataBindingMapper.class更新;执行ProcessBindable生成BR.class,并触发DataBindingMapper.class更新;

7 细节补充-View Tag的使用

第二章有讲到View是如何注入的,其实需要分两种情况:

  • 1.如果这个View标签属性中只有id,没有其他"@{表达式}"形式,则按照第2章提到的方式直接通过id查找;
  • 2.如果这个View标签属性中有"@{表达式}"形式的值,则编译器会自动给这个View加个android:tag="binding_{N}", 其中{N}按顺序从0开始递增,如android:tag="binding_0"。当执行ViewDataBinding#mapBindings去注入View时,会找tag为binding_开头的View,随后执行View注入;

另外,如果View标签原来就有android:tag值,则编译器会先保存原有值信息,写入android:tag="binding_{N}"。当执行完view注入后,再把原来的值赋值给android:tag。注意如果原来的android:tag值为"binding_0",那么在View注入时将会发生错乱。

在完成View注入后,ActivityDetail3Binding会执行this.setRootTag(root),代码如下:

public class ActivityDetail3Binding extends ViewDataBinding {

    public ActivityDetail3Binding(DataBindingComponent bindingComponent, View root) {
        super(bindingComponent, root, 0);
        Object[] bindings = mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds);
        this.detailActionButton = (Button)bindings[4];
        this.detailActionButton.setTag((Object)null);
        ...
        this.setRootTag(root);
        this.invalidateAll();
    }
}

这与ListView中的ViewHoloder实现方式相似,所以如果把DataBinding运用到ListView的ViewHolder中,就不需要多生成一个ViewHolder,直接使用这个ViewDataBinding类即可,例如ListAdapter实现:

public static class ListAdapter extends BaseAdapter{
    ...

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ItemFooBinding binding;
        if (convertView == null){
            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
            binding = DataBindingUtil.inflate(inflater,R.layout.item_foo,parent,false);
        } else {
            binding = DataBindingUtil.getBinding(convertView);
        }
        if (binding == null) return null;

        bind(binding,position);
        return binding.getRoot();
    }

    private void bind(ItemFooBinding binding, int position) {
        binding.title.setText("position:" + position);

        Context context = binding.avatar.getContext();
        int colorId = position % 2 == 0  ? R.color.colorAccent : R.color.colorPrimary;
        binding.avatar.setBackgroundColor(ContextCompat.getColor(context,colorId));
    }
}

8 总结

  • DataBinding 库非常小
    目前Android Data Binding在运行类库只有632个方法数,算上每个layout.xml自动生成的ViewDataBinding子类(demo中每个类不超过20个方法数),方法数总和也非常有限。


    Data Binding方法数
  • DataBinding 运行时没有多余性能损耗
    DataBinding所有的View注入、View赋值、Binding都是编译器自动生成的代码,这些重复的体力劳动本身就需要去做,只是交给了编译器来完成,所以运行时没有多余的性能损耗。
  • DataBinding 可以减少错误率
    既然View注入、View赋值、Binding都是编译器自动完成的,只要使用正确,100%无低级错误保证,可以提高代码质量,让开发者心情愉悦。
  • DataBinding 对编译时长的影响
    还没实际运用到生产环境,肯定有所延长,具体量级还未知。

9 Reference

官方Data-Binding-Guide
杨辉的个人博客-(译)Data Binding 指南
LyndonChin/MasteringAndroidDataBinding
googlesource/data-binding

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容