天天看点

Jetpack之DataBinding绑定布局文件是怎样实现的?

作者:搬砖小码龙
  • dataBinding的实现原理是什么?
  • dataBinding是怎么进行数据双向驱动的?

时间从来不语,却回答了所有。——致自己

本文从定义,用法,原理分析,由浅到深对DataBinding的实现原理进行挖掘,方便各位读者理解。篇幅较长,请耐心阅读。

  1. 定义:使用声明形式将布局中的界面组件绑定到应用中的数据源。数据的改变直接驱动UI的变化。
  2. 视图绑定:
  • 该模块的 build.gradle 文件中将 dataBinding 构建选项设置为 true。
android {
        ...
        //第一种
        dataBinding {
            enabled = true
        }
        //第二种 
        dataBinding.enabled = true
        //第三种
        buildFeatures {
        viewBinding true
        }
    }
               
  • 将xml布局中的根布局改成layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>

    </data>
    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity">
    </LinearLayout>
</layout>           
  • 在Activity中使用视图绑定
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //第一种 直接绑定
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                this, R.layout.activity_main)
        //第二种 可以使用 LayoutInflater 获取视图
         val binding: ActivityMainBinding = ActivityMainBinding.inflate
         (getLayoutInflater())
         setContentView(binding.root)
    }
            
  • 在Fragment中使用视图绑定

如果要在 Fragment、ListView 或 RecyclerView 适配器中使用数据绑定项,使用绑定类或 DataBindingUtil 类的 inflate() 方法,如以下代码示例所示:

val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

               

3 数据双向绑定

DataBinding除了可以进行布局绑定之外,还可以对布局view进行数据绑定。

<?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </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="@{user.firstName}"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"/>
       </LinearLayout>
    </layout>

               

在根标签layout中添加一个data标签,name为数据bean的别名,type为数据bean的全类名。然后通过给TextView的text属性,布局中的表达式使用“@{}”语法写入特性属性中进行数据绑定。android:text="@{user.firstName}",。在这里,TextView 文本被设置为 user 变量的 firstName 属性。

数据对象User

data class User(val firstName,val lastName)           

然后再Activity中,通过binding.user = User("","")将user变量绑定到布局视图上。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

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

        binding.user = User("Test", "User")
    }

               

这样我们就完成了一个简单是数据到布局视图上的绑定,如果我们修改了user中的某个属性值,视图也会更新吗?我们接着往下看。

Databinding不仅提供了绑定视图的功能,还提供了动态更新的功能。通过使用可观察的数据对象,通知布局自动更新。

class User {
        val firstName = ObservableField<String>()
        val lastName = ObservableField<String>()
        val age = ObservableInt()
    }           

首先我们将User对象中的数据定义为可观察的对象属性,当我们修改其中某个变量值得时候,会主动通知布局更新。除此之外还有没有其他办法实现?这个当然有,我们接着往下看。

class User : BaseObservable() {

        @get:Bindable
        var firstName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.firstName)
            }

        @get:Bindable
        var lastName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.lastName)
            }
    }
               

通过实现Observable接口将User对象变成一个可观察的对象,以便它们接收有关可观察对象的属性更改的通知。Observable 接口具有添加和移除监听器的机制,但何时发送通知必须由您决定。为便于开发,数据绑定库提供了用于实现监听器注册机制的 BaseObservable 类。实现 BaseObservable 的数据类负责在属性更改时发出通知。具体操作过程是向 getter 分配 Bindable 注释,然后在 setter 中调用 notifyPropertyChanged() 方法,如以下示例所示:

class User : BaseObservable() {

        @get:Bindable
        var firstName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.firstName)
            }

        @get:Bindable
        var lastName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.lastName)
            }
    }
               

数据绑定在模块包中生成一个名为 BR 地类,该类包含用于数据绑定的资源的 ID。在编译期间,Bindable 注释会在 BR 类文件中生成一个条目。如果数据类的基类无法更改,Observable 接口可以使用 PropertyChangeRegistry 对象实现,以便有效地注册和通知监听器。

4 原理分析

Jetpack之DataBinding绑定布局文件是怎样实现的?

我们首先来看一下DataBindingUtil是如何绑定xml布局的:DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId, @Nullable DataBindingComponent bindingComponent) {
        //调用当前activity的setContentView方法
        activity.setContentView(layoutId);
        View decorView = activity.getWindow().getDecorView();
        //通过findViewById获取根布局
        ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
        return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
    }
            
private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
        //获取子view个数
        final int endChildren = parent.getChildCount();
        //添加了多少个view
        final int childrenAdded = endChildren - startChildren;
        //当只有一个子view时,直接获取ziview
        if (childrenAdded == 1) {
            final View childView = parent.getChildAt(endChildren - 1);
            return bind(component, childView, layoutId);
        } else {
            //当数量大于1个时,创建view数组用来接收子view
            final View[] children = new View[childrenAdded];
            for (int i = 0; i < childrenAdded; i++) {
                children[i] = parent.getChildAt(i + startChildren);
            }
            return bind(component, children, layoutId);
        }
    }           

从源码中可以看出不管是只有一个子View还是多个子View,最终都是调用bind()方法,我们接着往下看。

static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
    }           

通过调用bind方法,我们看到sMapper.getDataBinder 返回一个DataBinding对象,那这个getDataBinder方法是怎么返回的呢,我们点进去发现调用到DataBinderMapper.getDataBinder,DataBindingMapper是个抽象类,那我们只能从其子类DataBinderMapperImpl入手。

public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
        int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
        if (localizedLayoutId > 0) { //获取布局
            Object tag = view.getTag();
            if (tag == null) {
                throw new RuntimeException("view must have a tag");
            }

            switch (localizedLayoutId) {
                case 1:
                    //如果tag与这个标记相等 就new一个ActivityMainBindingImpl 返回
                    if ("layout/activity_main_0".equals(tag)) {
                        return new ActivityMainBindingImpl(component, view);
                    }

                    throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
            }
        }

        return null;
    }           

从DataBinderMapperImpl的getDataBinder中,我们终于看到了ActivityMainBindingImpl被创建,ActivityMainBindingImpl是ActivityMainBinding的实现类,至此我们终于知道val activityMainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)是如果被创建返回的了。

看到这里很多人有疑问了? "layout/activity_main_0".equals(tag) 这判断是怎么来的,明明自己没有在布局中没有设置tag标签,那这个tag是从哪来的?带着这个疑问我们进一步深入研究一下。首先我们要知道Databinding是基于APT技术动态生成的,比如上面的ActivityMainBindingImpl等代码都是通过编译自动生成。那么有没有一种可能 这个tag标签也是自动生成插入的。我们往下看。

Jetpack之DataBinding绑定布局文件是怎样实现的?

我们找到编译后的activity_main-layout.xml文件,看一下编译器为我们做了哪些工作?

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Layout layout="activity_main" modulePackage="com.example.kotlinproject"
        filePath="app\src\main\res\layout\activity_main.xml" directory="layout"
        isMerge="false" isBindingData="true"
        rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
    <Targets>
         //编译器自动为我们插入一个tag
        <Target tag="layout/activity_main_0" view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions/>
            <location startLine="6" startOffset="4" endLine="19" endOffset="55"/>
        </Target>
        <Target id="@+id/tv" view="TextView"> 
            <Expressions/>
            <location startLine="10" startOffset="8" endLine="18" endOffset="58"/>
        </Target>
    </Targets>
</Layout>           

我们从编译后的布局文件中可以看到,编译器在第八行 自动为我们插入了一个tag="layout/activity_main_0" 标签。用来代替我们在layout根标签的布局文件。至此我们知道了"layout/activity_main_0".equals(tag)这个判断添加是怎么来的了 ,由编译器自动为我们生成而来。

喜欢这篇文章的小伙伴,欢迎评论区留言,麻烦点个关注或收藏哦,您的支持就是小编创作的最大动力!