【Android Jetpack高手日志】DataBinding 从入门到精通

前言

DataBinding 数据绑定库是 Android Jetpack 的一部分,借助该库可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。我个人觉得,使用 DataBinding 时不要在 xml 布局文件中写复杂的逻辑,只负责绑定数据。只是负责将最终的数据和 UI 直接绑定,只是一个末端赋值而已,不涉及复杂的 UI 逻辑,而且避免了代码中大量冗余代码的判空处理,同时避免了那些常见的 setVisible 等样板方法的调用,简化开发流程,统一 UI 的数据来源。

基本使用

简单使用入门

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>
        <import type="com.jackie.jetpackdemo.data.TestInfo"/>
        <variable
            name="userInfo"
            type="TestInfo" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:id="@+id/btnGetUserInfo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="获取用户信息"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/txtUserName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@{userInfo.age}"
            app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
            android:layout_marginTop="30dp"
            android:textSize="30dp"

            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="text"
            android:gravity="center"
            app:layout_constraintTop_toBottomOf="@+id/txtUserName"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Activity 中调用代码如下:

override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     val activityBinding: ActivityMainBinding = 		  DataBindingUtil.setContentView(this,R.layout.activity_main)
        activityBinding.lifecycleOwner = this
        activityBinding.userInfo = TestInfo("lsm","lsj")
}

TestInfo 的定义如下:

public class TestInfo extends BaseObservable { //继承 BaseObservable 
    private String age;
    private String name;
    public TestInfo(String age,String name){
        this.name = name;
        this.age = age;
    }
    public void setAge(String age) {
        this.age = age;
        notifyPropertyChanged(BR.age);  //需要变更的变量的 set 方法中加上 notifyPropertyChanged
    }
    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name); //需要变更的变量的 set 方法中加上 notifyPropertyChanged
    }
    @Bindable											//需要变更的变量还要加上 @Bindable 注解
    public String getAge() {
        return age;
    }
    @Bindable										 //需要变更的变量还要加上 @Bindable 注解
    public String getName() {
        return name;
    }
}

TestInfo需要继承BaseObservable,同时对于需要监听变化的变量加上@Bindable 注解,同时该变量的set方法还要加上notifyPropertyChangedBR.xxx 是注解生成的。

数据的双向绑定

使用单向数据绑定时,您可以为特性设置值,并设置对该特性的变化作出反应的监听器:

<CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@{viewmodel.rememberMe}"
        android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
    />

双向数据绑定为此过程提供了一种快捷方式:

<CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@={viewmodel.rememberMe}"
    />

@={} 表示法(其中重要的是包含“=”符号)可接收属性的数据更改并同时监听用户更新。其他的设置和前面的单向数据绑定一致。

结合 LiveData 使用

内容参考自这里,我们上面在使用 DataBinding 时,TestInfo 还要继承 BaseObserble,使用注解、notifyPropertyChanged(),使用起来其实挺复杂,而且还有侵入性。LiveData 结合 DataBinding 的使用步骤如下:

  1. 使用 LiveData 对象作为数据绑定来源,需要设置 LifecycleOwner。
  2. xml 中定义变量 ViewModel,并使用 ViewModel。
  3. binding 设置变量 ViewModel。
    //结合DataBinding使用的ViewModel
    //1\. 要使用LiveData对象作为数据绑定来源,需要设置LifecycleOwner
    binding.setLifecycleOwner(this);

    ViewModelProvider viewModelProvider = new ViewModelProvider(this);
    mUserViewModel = viewModelProvider.get(UserViewModel.class);
    //3\. 设置变量ViewModel
    binding.setVm(mUserViewModel);

xml 文件的定义如下:

        <!-- 2\. 定义ViewModel 并绑定-->
	<variable
            name="vm"
            type="com.hfy.demo01.module.jetpack.databinding.UserViewModel" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.userLiveData.name}"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{vm.userLiveData.level}"/>

这样就ok了,你会发现 我们不需要在 Activity 中拿到 LivaData 去 observe(owner,observer)了,DataBinding 自动生成的代码,会帮我们去做这操作,所以需要设置LifecycleOwner。

使用自定义特性的双向数据绑定

例如,如果要在名为 MyView 的自定义视图中对 "time" 特性启用双向数据绑定,请完成以下步骤:

  1. 使用 @BindingAdapter,对用来设置初始值并在值更改时进行更新的方法进行注释:
    @BindingAdapter("time")
    @JvmStatic fun setTime(view: MyView, newValue: Time) {
        // Important to break potential infinite loops.
        if (view.time != newValue) {
            view.time = newValue
        }
    }
  1. 使用 @InverseBindingAdapter 对从视图中读取值的方法进行注释:
    @InverseBindingAdapter("time")
    @JvmStatic fun getTime(view: MyView) : Time {
        return view.getTime()
    }

更多内容请参考这里

源码分析

我们在路径app/build/intermediates/data_binding_layout_info_type_merge/debug/out/activity_main-layout.xml查看文件

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout directory="layout" filePath="app/src/main/res/layout/activity_main.xml"
    isBindingData="true" isMerge="false" layout="activity_main"
    modulePackage="com.jackie.jetpackdemo" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
    <Variables name="userInfo" declared="true" type="TestInfo">
        <location endLine="9" endOffset="29" startLine="7" startOffset="8" />
    </Variables>
    <Imports name="TestInfo" type="com.jackie.jetpackdemo.data.TestInfo">
        <location endLine="6" endOffset="60" startLine="6" startOffset="8" />
    </Imports>
    <Targets>
        <Target tag="layout/activity_main_0"
            view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions />
            <location endLine="47" endOffset="55" startLine="12" startOffset="4" />
        </Target>
        <Target id="@+id/txtUserName" tag="binding_1" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="userInfo.age">
                    <Location endLine="32" endOffset="41" startLine="32" startOffset="12" />
                    <TwoWay>false</TwoWay>
                    <ValueLocation endLine="32" endOffset="39" startLine="32" startOffset="28" />
                </Expression>
            </Expressions>
            <location endLine="37" endOffset="13" startLine="27" startOffset="8" />
        </Target>
        <Target id="@+id/btnGetUserInfo" view="Button">
            <Expressions />
            <location endLine="25" endOffset="55" startLine="17" startOffset="8" />
        </Target>
    </Targets>
</Layout>

可以看到<Targets>标签下面的就是我们布局,分成具体的子<Target>标签对应具体的 ConstraintLayout、TextView等,activity_main_0 对应我们的ConstraintLayout,再来路径app/build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity" android:tag="layout/activity_main_0" 		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">

        <Button
            android:id="@+id/btnGetUserInfo"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="获取用户信息"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/txtUserName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:tag="binding_1"       
            app:layout_constraintTop_toBottomOf="@+id/btnGetUserInfo"
            android:layout_marginTop="30dp"
            android:textSize="30dp"

            />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="text"
            android:gravity="center"
            app:layout_constraintTop_toBottomOf="@+id/txtUserName"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>

这个文件其实就是移除了<layout>、<data>标签的布局文件,里面的 tag 就是我们上面对应<Target>标签中的tagExpression attribute="android:text" text="userInfo.age" <Expression>中的具体属性对应具体的值。

初始化

再来从val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)来分析源码,

//DataBindingUtil.java    
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId, @Nullable DataBindingComponent bindingComponent) {
        activity.setContentView(layoutId); //还是需要setContentView
        View decorView = activity.getWindow().getDecorView();
        ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
        return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
    }

我们设置的activity_xxx.xml其实是在android.R.id.content下面的,继续来看bindToAddedViews方法

//DataBindingUtil.java      
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
        final int endChildren = parent.getChildCount();
        final int childrenAdded = endChildren - startChildren;
        if (childrenAdded == 1) {
            final View childView = parent.getChildAt(endChildren - 1);
            return bind(component, childView, layoutId); //调用bind
        } else {
            final View[] children = new View[childrenAdded];
            for (int i = 0; i < childrenAdded; i++) {
                children[i] = parent.getChildAt(i + startChildren);
            }
            return bind(component, children, layoutId); //调用bind
        }
    }
}

最终会调到bind方法

//DataBindingUtil.java  
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

sMapperDataBinderMapper,其真正实现类是通过 APT 生成的DataBinderMapperImpl(app/build/generated/ap_generated_sources/debug/out/com/jackie/jetpackdemo/DataBinderMapperImpl.java)

public class DataBinderMapperImpl extends DataBinderMapper {
···
  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    if(localizedLayoutId > 0) {
      final Object tag = view.getTag();
      if(tag == null) {
        throw new RuntimeException("view must have a tag");
      }
      switch(localizedLayoutId) {
        case  LAYOUT_ACTIVITYMAIN: {
          if ("layout/activity_main_0".equals(tag)) {
            return new ActivityMainBindingImpl(component, view);  //关键代码,new ActivityMainBindingImpl
          }
			···

接下来我们来分析ActivityMainBindingImpl(app/build/generated/ap_generated_sources/debug/out/com/jackie/jetpackdemo/databinding/ActivityMainBindingImpl.java)这个类,它也是 APT 生成的,

//ActivityMainBindingImpl.java	    
		public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
    }
    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 1
            , (android.widget.Button) bindings[2]
            , (android.widget.TextView) bindings[1]
            );
        this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.txtUserName.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

我们调用了第一个方法,里面的这个 3 代表着我们布局文件中有三个节点(ConstraintLayout,Button,TextView),但是我们前面的布局中明明还有一个TextView,为什么没有呢?因为我们这个TextView我们并没有设置它的 Id,所以没有生成,如果设置后重新 build 下 3 就会变成 4 了。

继续来看mapBindings方法:

    protected static Object[] mapBindings(DataBindingComponent bindingComponent, View root,
            int numBindings, IncludedLayouts includes, SparseIntArray viewsWithIds) {
        Object[] bindings = new Object[numBindings];
        mapBindings(bindingComponent, root, bindings, includes, viewsWithIds, true);
        return bindings;
    }

它首先是 new 一个大小为 3 的对象数组,然后把这三个标签解析完放到该数组中。上面的ActivityMainBindingImpl公有构造器会调用私有构造器,再回过头来看

val activityBinding: ActivityMainBinding = DataBindingUtil.setContentView(this,R.layout.activity_main)

执行完这个代码,activityBinding中已经有这 3 个对象了,所以可以进行这样的调用

        activityBinding.txtUserName
        activityBinding.btnGetUserInfo

到目前为止,初始化已经完成了。

调用流程

接下来我们从这个调用开始分析

activityBinding.userInfo = TestInfo("lsm","lsj")

这个activityBinding.userInfo调用的实际上是ActivityMainBinding中的setUserInfo方法

    //ActivityMainBinding.java
    public void setUserInfo(@Nullable com.jackie.jetpackdemo.data.TestInfo UserInfo) {
        updateRegistration(0, UserInfo);
        this.mUserInfo = UserInfo;
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.userInfo);
        super.requestRebind();
    }

这里的updateRegistration方法如下

//localFieldId 为 BR 文件中的Id,observable 就是观察者
protected boolean updateRegistration(int localFieldId, Observable observable) {
        return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
    }
    /**
     * Method object extracted out to attach a listener to a bound Observable object.
     */
    private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
        @Override
        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
            return new WeakPropertyListener(viewDataBinding, localFieldId).getListener();
        }
    };

CREATE_PROPERTY_LISTENER名字也很直白,表示创建一个属性的监听器,也就是说属性发生变化的时候WeakPropertyListener监听器会被回调。

localFieldId 为 BR 文件中的 Id,BR 文件是什么呢?

public class BR {
  public static final int _all = 0;
  public static final int age = 1;
  public static final int name = 2;
  public static final int userInfo = 3;
}

因为我们的 xml 文件中导入了TestInfo(userInfo),我们也在agename属性上加上了@Bindable注解,所以生成了上面的BR文件。因为我们是上面调用的是setUserInfo方法,所以传入的是 0。

用该方式设置name也可以

activityBinding.setVariable(BR.name,"Jackie")

updateRegistration中的observable就是我们传入的(TestInfo)UserInfo,再来看看updateRegistration方法

//ViewDataBinding    
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;
    }

mLocalFieldObservers数组中绑定了每一个属性对应的监听器,比如我们上面的 BR 中的四个值。

如果监听器为空,调用registerTo创建监听器并注册

    protected void registerTo(int localFieldId, Object observable,
            CreateWeakListener listenerCreator) {
        if (observable == null) {
            return;
        }
        WeakListener listener = mLocalFieldObservers[localFieldId];
        if (listener == null) {
            listener = listenerCreator.create(this, localFieldId);
            mLocalFieldObservers[localFieldId] = listener;
            if (mLifecycleOwner != null) {
                listener.setLifecycleOwner(mLifecycleOwner);
            }
        }
        listener.setTarget(observable);
    }

setTarget就是给被观察者添加监听器

        public void setTarget(T object) {
            unregister();
            mTarget = object;
            if (mTarget != null) {
                mObservable.addListener(mTarget);
            }
        }

这里的mObservable的实现类是WeakPropertyListener,也就是每个属性发生变化后都会进行回调

@Override
public void addListener(Observable target) {
    target.addOnPropertyChangedCallback(this);
}

target的实现类是BaseObservable,这也就是我们的TestInfo为什么要继承BaseObservable了。

public class BaseObservable implements Observable {
    private transient PropertyChangeRegistry mCallbacks;

    public BaseObservable() {
    }

    @Override
    public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
        synchronized (this) {
            if (mCallbacks == null) {
                mCallbacks = new PropertyChangeRegistry();
            }
        }
        mCallbacks.add(callback);
    }

总的关系图如下:

在这里插入图片描述

PropertryChangeRegistry中的add(ViewDataBinding)是将观察者和被观察者绑定起来,ViewDataBinding中的WeakListener[] mLocalFieldObservers中的每个变量都有一个WeakListenerBaseObservable中的addOnPropertyChangedCallback(WeakPropertyListener)就是添加属性变化回调。

MainActivity 调用 setUserInfo 流程图

如果上面的关系图不清楚的话,我也把流程图画出来,你可以查看一下

ActivityMainBindingImpl继承于ActivityMainBinding,而ActivityMainBinding继承于ViewDataBinding

最终是在TextViewBindingAdaptersetText来实现。

    @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);
    }
没调用 setUser 之前的数据绑定是怎么做的?

前面我们讲了初始化过程,因为ActivityMainBindingImpl继承于ActivityMainBinding,而ActivityMainBinding继承于ViewDataBinding,在ViewDataBinding中静态初始化块如下

    static {
        if (VERSION.SDK_INT < VERSION_CODES.KITKAT) {
            ROOT_REATTACHED_LISTENER = null;
        } else {
            ROOT_REATTACHED_LISTENER = new OnAttachStateChangeListener() {
                @TargetApi(VERSION_CODES.KITKAT)
                @Override
                public void onViewAttachedToWindow(View v) {
                    // execute the pending bindings.
                    final ViewDataBinding binding = getBinding(v);
                    binding.mRebindRunnable.run();
                    v.removeOnAttachStateChangeListener(this);
                }

                @Override
                public void onViewDetachedFromWindow(View v) {
                }
            };
        }
    }

会执行mRebindRunnablerun方法

    private final Runnable mRebindRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (this) {
                mPendingRebind = false;
            }
            processReferenceQueue();

            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();
        }
    };

然后执行executePendingBindings方法,后面就和我们前面分析过的的流程一样了。最终会会调到

    @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);
    }

我们一开始text为空,所以就直接返回了。

为什么设计如此复杂,以前我们用的话观察者和被观察者都是单独的类,现在因为可能有多个ViewModel(TestInfo、xxxInfo等),同时也可能有多个Activity(分别进行绑定),使用原来的方式消耗很大。BR 文件会生成多个字段,用一个mLocalFieldObservers进行处理,每个字段有其具体的监听,每个xxxInfo 有其具体的监听。设计的还是挺合理的。

总结

本文先从 DataBinding 的简单使用,单向/双向绑定,结合LiveData 使用,同时也可以使用一些自定义特性,最后是进行源码分析。最后一定要注意的是不要在 xml 中做复杂的逻辑判断,只是将其看做是一个方便末端 UI 展示的支持库即可,同时也避免了大量的判空处理,同时也统一了数据的来源。

献给读者

随着互联网企业的不断发展,产品项目中的模块越来越多,用户体验要求也越来越高,想实现小步快跑、快速迭代的目的越来越难,还有65535,应用之间的互相调用等等问题,插件化技术应用而生。如果没有插件化技术,美团、淘宝这些集成了大量“app”的应用,可能会有几个g那么大。

所以,当今的Android移动开发,不会热修复、插件化、组件化,80%以上的面试都过不了。

Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android开源框架。系统教程知识笔记已整理成PDF电子书上传在【GitHub】

1042页完整版PDF点击我就可以白嫖啦,记得给文章点个赞哦。

文末

感谢大家关注我,分享Android干货,交流Android技术。
对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,我会虔诚为你解答。
也欢迎大家来我的B站找我玩,有各类Android架构师进阶技术难点的视频讲解,助你早日升职加薪。
B站直通车:https://space.bilibili.com/544650554

猜你喜欢

转载自blog.csdn.net/Androiddddd/article/details/115865361
今日推荐