Jetpack-Databinding

672 阅读4分钟

众所周知, 使用dataBinding是为了配合app的mvvm架构,建立数据与ui的绑定关系, 数据更新驱动ui变化; 建立双向绑定,ui变化也能改变数据

一.Databinding的简单使用

步骤一: 创建要更新的数据

class AccountInfo : BaseObservable() {
    var account: String? = null
        @Bindable
        get() {
            return field
        }
        set(value) {
            field = value
            notifyPropertyChanged(BR.account)
        }
}

注意点:在编译时如果遇到找不到@Bindable或者BR,在app下的build.gradle 引入 'kotlin-kapt',同步完后build当前项目

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}
kapt {
    generateStubs = true
}

步骤二:创建布局文件, 关联数据

创建完成后, 需要引入layout标签, 然后引入关联的数据, 绑定到指定的视图上

<?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">
    <!-- 引入的关联数据 -->
    <data>
        <variable
            name="accountInfo"
            type="com.wislie.jetpack_kotlin_demo.AccountInfo" />
​
    </data>
​
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
​
        <!-- @{accountInfo.account}设置数据与视图的单向绑定关系-->
        <TextView
            android:id="@+id/tv_account"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#1fbbee"
            android:textSize="28sp"
            android:text="@{accountInfo.account}"/>
​
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

注意点: option+Enter键 生成layout布局

步骤三: 创建ActivityXXBinding, 通过ActivityXXBinding建立数据与视图的绑定关系

创建ActivityXXBinding, 同时创建数据, 将此数据赋值给binding

class MMVMTestActivity:AppCompatActivity() {
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMvvmTestBinding>(
                this,R.layout.activity_mvvm_test)
        val accountInfo = AccountInfo()
        binding.accountInfo = accountInfo
        binding.lifecycleOwner = this
​
        val accountArr = arrayListOf("1200","1300","1400")
        var index = 0
        thread {
            while (true){
                //每隔3秒,将数据更新到ui上
                accountInfo.account = accountArr[index++%accountArr.size]
                Thread.sleep(3000)
            }
        }
​
    }
}

实现的效果:每隔3秒,ui上依次更新为1200, 1300,1400

二.DataBinding的实现原理

DataBinding采用的是注解处理器技术,编译之后会将上述的布局文件拆分成两个部分, 一个是app能识别的布局文件,前者布局文件中指明了id的视图控件, 会在另一个xml生成对应的tag; Databinding管理布局时会通过tag查找到控件视图, 当数据发生改变,会将数据设置到对应的视图控件中

2.1 layout标签的布局文件,apt生成2个文件

文件一: 页面显示的布局文件activity_mvvm_test.xml,在incremental->mergeDebugResources->stripped.dir->layout目录下

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent" android:tag="layout/activity_mvvm_test_0" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
​
    <!-- @{accountInfo.account}建立数据与视图的单向绑定关系-->
    <TextView
        android:id="@+id/tv_account"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#1fbbee"
        android:textSize="28sp"
        android:tag="binding_1"              />
​
</androidx.constraintlayout.widget.ConstraintLayout>

文件二: activity_mvvm_test-layout.xml,该文件在data_binding _layout_info_type_merge->debug->out目录下,原来的标签和标签, 控件中关联绑定的变量都在此文件中

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="jetpack_kotlin_demo/src/main/res/layout/activity_mvvm_test.xml"
    isBindingData="true" isMerge="false"
    layout="activity_mvvm_test" modulePackage="com.wislie.jetpack_kotlin_demo"
    rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
    <Targets>
        <Target tag="layout/activity_mvvm_test_0"
            view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions />
        </Target>
        <Target id="@+id/tv_account" tag="binding_1" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="accountInfo.account">
                </Expression>
            </Expressions>
        </Target>
    </Targets>
    <Variables name="accountInfo" declared="true" type="com.wislie.jetpack_kotlin_demo.AccountInfo">
    </Variables>
</Layout>

注意点:只有声明了id的控件,才会有tag, 布局根目录的控件tag是以"layout_"开头,普通控件的tag是以"binding下划线"开头

2.2 DataBindingUtil.setContentView

追踪进去可以看到设置会将布局设置到节点content中

activity.setContentView(layoutId);

接下来会将声明id的控件赋值给bindings数组

Object[] bindings = new Object[numBindings];
mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);

省略一些非关键代码, 如果view是ViewGroup, 会查找view的子控件,并赋值给bindings[index]

private static void mapBindings(DataBindingComponent bindingComponent, View view,
        Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
        boolean isRoot) {
    ///////////////////////////// 省略///////////////////////////// 
    Object objTag = view.getTag();
    final String tag = (objTag instanceof String) ? (String) objTag : null;
    boolean isBound = false;
    ///////////////////////////// 省略///////////////////////////// 
    if (isRoot && tag != null && tag.startsWith("layout")) {
        final int underscoreIndex = tag.lastIndexOf('_');
        ///////////////////////////// 省略///////////////////////////// 
        final int index = parseTagInt(tag, underscoreIndex + 1);
        if (bindings[index] == null) {
          bindings[index] = view;
        }
        isBound = true;
        ///////////////////////////// 省略///////////////////////////// 
    } else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
        int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
        if (bindings[tagIndex] == null) {
            bindings[tagIndex] = view;
        }
        isBound = true;
        ///////////////////////////// 省略///////////////////////////// 
    } 
    if (!isBound) {
        final int id = view.getId();
        if (id > 0) {
            int index;
            if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0 &&
                    bindings[index] == null) {
                bindings[index] = view;
            }
        }
    }
​
    if (view instanceof  ViewGroup) {
        final ViewGroup viewGroup = (ViewGroup) view;
        final int count = viewGroup.getChildCount();
        int minInclude = 0;
        for (int i = 0; i < count; i++) {
            final View child = viewGroup.getChildAt(i);
           
            ///////////////////////////// 省略///////////////////////////// 
            mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
            
        }
    }
}

bindings数组填充完成后,将绑定的数据更新到对应的ui上 invalidateAll->requestRebind->mFrameCallback.run()->executePendingBindings->executeBindingsInternal->executeBindings

executeBindings如下, 也就是textView.setText()

@Override
protected void executeBindings() {
    long dirtyFlags = 0;
    synchronized(this) {
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;
    }
    java.lang.String accountInfoAccount = null;
    com.wislie.jetpack_kotlin_demo.AccountInfo accountInfo = mAccountInfo;

    if ((dirtyFlags & 0x7L) != 0) {



            if (accountInfo != null) {
                // read accountInfo.account
                accountInfoAccount = accountInfo.getAccount();
            }
    }
    // batch finished
    if ((dirtyFlags & 0x7L) != 0) {
        // api target 1

        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvAccount, accountInfoAccount);
    }
}

2.3: 实时更新ui

accountInfo.account = accountArr[index++%accountArr.size]

赋值之后本质是调用了account的set函数

set(value) {
    field = value
    notifyPropertyChanged(BR.account)
}

-> BaseObservable PropertyChangeRegistry.notifyCallbacks(this, fieldId, null);

->....

经过层层调用, 会到ViewDataBinding的requestRebind, 回到了和步骤二将默认数据填充到ui上一样的流程了

注意点:绑定的成员变量可以使用Observable包装类,LiveData等

binding.lifecycleOwner = this

这一行的作用和之前LiveData中LifecycleBoundObserver的activeStateChanged 一样的意思, 当宿主处于活跃状态,即可见状态时, 可以进行更新UI, 源码ViewDataBinding的requestRebind方法如下

protected void requestRebind() {
    if (mContainingBinding != null) {
        mContainingBinding.requestRebind();
    } else {
        final LifecycleOwner owner = this.mLifecycleOwner;
        if (owner != null) {
            Lifecycle.State state = owner.getLifecycle().getCurrentState();
            if (!state.isAtLeast(Lifecycle.State.STARTED)) {
                return; // wait until lifecycle owner is started
            }
        }
        synchronized (this) {
            if (mPendingRebind) {
                return;
            }
            mPendingRebind = true;
        }
        if (USE_CHOREOGRAPHER) {
            mChoreographer.postFrameCallback(mFrameCallback);
        } else {
            mUIThreadHandler.post(mRebindRunnable);
        }
    }
}

requestRebind在什么时候调用, 设置默认值和绑定数据发生改变时.

目前有几个疑问:

1.如果绑定数据发生改变了, 调用requestRebind前突然锁屏了,解锁屏幕后,这样是否会造成数据的丢失?

2.activity.setContentView(layoutId) 此layoutId是哪个,是我们手写的布局,还是拆分的布局; 按道理应该是拆分的布局,是如何定位到的?