如何应对 Android 面试官 -> 玩转 Jetpack DataBinding

前言


本章重点讲解 DataBinding 及其原理;

DataBinding 的由来


DataBinding 的本质是实现 双向绑定 也就是数据变化UI更新,UI改变反映到数据上;

说起双向绑定,那么我们就要回溯一下 Android 开发上的 MVC -> MVP -> MVVM 的演进史,大家可以看下我前面的文章,关于 MVX 的介绍;

MVVM 中的 DataBinding 是为了解决 MVP 上的『回调地狱』,通过 DataBinding 完成 Activity(V) 和 VM 层的双向绑定;从而规避从 VM 层频繁回调给 V 层的『回调地狱』问题;

那么问题来了 DataBinding 是 MVVM 特有的吗?

答案:并不是,DataBinding 是 Android 提供的一个组件框架,是一个工具,用来实现数据的双向绑定,它不仅仅可以应用于 Android 上的 MVVM,还可以应用于 MVP;只不过大部分情况下,MVVM 会使用 DataBinding;

MVVM 适合什么样的业务?

个人感觉:MVVM 更适合界面更新比较频繁的业务;

基础使用


启用 DataBinding

arduino 复制代码
dataBinding {
   enabled true
}

后者

ini 复制代码
dataBinding.enabled = true

两种方式都可以实现 DataBinding 的启用;

接下来我们来声明数据 bean,也就是 Model,我们来声明一个 User 类

typescript 复制代码
// 核心逻辑1
public class User extends BaseObservable {

    private String name;
    private String pwd;

    public User(String name, String pwd) {
        this.name = name;
        this.pwd = pwd;
    }
    
    // 核心逻辑3
    @Bindable // BR里面标记生成 name数值标记
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        // 核心逻辑 2
        notifyPropertyChanged(BR.name); // APT 又是注解处理器技术 BR文件
    }

    // 核心逻辑3
    @Bindable // BR里面标记生成 pwd数值标记
    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
        // 核心逻辑 2
        notifyPropertyChanged(BR.pwd); // APT 又是注解处理器技术 BR文件
    }
}

核心逻辑1:我们声明的 Model 必须要继承 androidx.databinding.BaseObservable

核心逻辑2:我们声明的 set 方法中,必须要调用 notifyPropertyChanged() 方法;

核心逻辑3:我们声明的 get 方法用 @Bindable 注解标记;

接下来,我们来声明 xml 文件,支持 DataBinding;

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>

<!-- layout 是 DataBinding 管理了我们整个布局了 -->
<!-- 核心逻辑1 -->
<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">

    <!-- 核心逻辑2 -->
    <data>
        <variable
            name="user"
            type="com.llc.databinding.User" />
    </data>
    <!-- 上面的是 DataBinding 内部用的 -->

    <!-- Android View 体系的,下面的所有内容,会给 Android 绘制 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            <!-- 核心逻辑3 -->
            android:text="@{user.name}"
            android:textSize="50sp" />

        <TextView
            android:id="@+id/tv2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.pwd}"
            android:textSize="50sp" />
    </LinearLayout>
</layout>

核心逻辑1:xml 文件必须要用 <layout>标签进行包裹;

核心逻辑2:xml 中声明<data>标签 和 <variable>,用来引入 Model 层声明的数据 bean;

核心逻辑3:xml 中声明的 View 的 text 属性使用@{}赋值;

APT 技术会拆分这个 xml 文件,上面部分给 DataBinding 使用,下面部分给 Android 的 View 体系进行绘制;

所以编译之后,DataBinding 会帮我们生成拆分后的两个文件;

一个是 activiry_main-layout.xml 文件

一个是 activity_main.xml 文件,剔除 <layout>标签的文件;

接下来,在 Activity 中将 xml 通过 DataBinding 绑定起来;

scala 复制代码
public class MainActivity extends AppCompatActivity {
    
    /** 数据 Model */
    User user;
    /** binding */
    ActivityMainBinding binding
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        user = new User("llc", "123");
        binding.setUser(user); // 必须要建立绑定关系,否则没有任何效果
    }
}

看到这里的时候,有人会有疑问了,这个 setUser 是怎么来的?

ini 复制代码
<variable
    name="user"
    type="com.derry.databinding_java.User" />

这个 setUser 方法就是根据 name 的 value "user" 来的,如果是 aa 那么就是 setAa 方法;

单向绑定(Model -> View)

接下来,我们来看下 model 变化,不用调用 textView.setText 就能更新 UI 的例子:

csharp 复制代码
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(1000);
                // 核心逻辑
                user.setName(user.getName() + "i");
                user.setPwd(user.getPwd() + "i");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}).start();

运行,可以看下效果,运行后,我们并没有调用 setText 方法,但是,我们的 View 确确实实实的更新了,就是 DataBinding 起作用了;

双向绑定(View -> Model and Model -> View)

接下来,我们来看下 View 变化,如何更新到 Model;我们需要将 xml 的 text 部分修改下:

ini 复制代码
<TextView
   android:id="@+id/tv2"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@={user.pwd}"
   android:textSize="50sp" />

需要加一个 =来标识是 双向绑定

Activity 中修改成我们来打印 Model 中的数据,通过 修改 EditText 中的数据;

arduino 复制代码
new Handler().postDelay({
    Log.d("TAG", "name: " + user.name + ", pwd: " + user.pwd);
}, 10000);

运行,可以看到,当我们修改了 EditText 中的内容之后,会打印修改后的内容;

但是,我们实际开发中,大部分开发者都是只使用了单向绑定(Model -> View)的绑定能力,双向绑定其实还是比较耗费性能的,因为它要实时监控 View 的变化;

原理篇


使用了 DataBinding 的布局变化

经过编译之后,DataBinding 会帮我们把 activity_mainxml 拆分成两个文件,一个是 activity_main-layout.xml,一个是 activity_main.xml 文件;

这两个文件,我们先来看 activity_main-layout.xml 文件;

ini 复制代码
<?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.derry.databinding_java" rootNodeType="android.widget.LinearLayout">
    <Variables name="user" declared="true" type="com.derry.databinding_java.User">
        <location endLine="12" endOffset="52" startLine="10" startOffset="8" />
    </Variables>
    <Targets>
        <Target tag="layout/activity_main_0" view="LinearLayout">
            <Expressions />
            <location endLine="43" endOffset="18" startLine="23" startOffset="4" />
        </Target>
        <Target id="@+id/tv1" tag="binding_1" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="user.name">
                    <Location endLine="34" endOffset="39" startLine="34" startOffset="12" />
                    <TwoWay>true</TwoWay>
                    <ValueLocation endLine="34" endOffset="37" startLine="34" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="35" endOffset="37" startLine="30" startOffset="8" />
        </Target>
        <Target id="@+id/tv2" tag="binding_2" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="user.pwd">
                    <Location endLine="41" endOffset="38" startLine="41" startOffset="12" />
                    <TwoWay>true</TwoWay>
                    <ValueLocation endLine="41" endOffset="36" startLine="41" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="42" endOffset="37" startLine="37" startOffset="8" />
        </Target>
    </Targets>
</Layout>

这个文件是通过提取 activity_main.xml 文件中 layout 部分以及每个控件的细节,然后生成的文件;

接下来,我们来看下生成的 activity_main.xml 文件

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:tag="layout/activity_main_0">

    <!-- TextView -->
    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_1"
        android:textSize="50sp" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_2"
        android:textSize="50sp" />
</LinearLayout>

可以看到,它给每个控件都动态的添加了一个 tag 属性;这个 tag 属性,是为了和 activity_main-layout.xml 中的<Target>中的 tag 进行绑定;

如何 findViewById

我们接下来看下 DataBinding 如何 setContentView 的,我们进入这个方法看下:

less 复制代码
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
        int layoutId, @Nullable DataBindingComponent bindingComponent) {
    // 核心逻辑1    
    activity.setContentView(layoutId);
    // 核心逻辑2
    View decorView = activity.getWindow().getDecorView();
    // 核心逻辑3
    ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
    // 核心逻辑4
    return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}

核心逻辑1:调用当前 activity 的 setContentView;

核心逻辑2:获取当前 activity 的 decorView;

核心逻辑3:获取布局渲染的根布局 View;

核心逻辑4:替换之前的 xml;

我们进入核心逻辑4的 bindToAddViews 方法看下:

ini 复制代码
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);
    } 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 方法看下:

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

这里的 sMapper 就是

java 复制代码
private static DataBinderMapper sMapper = new DataBinderMapperImpl();

我们进入这个 DataBinderMapperImpl 看下:

csharp 复制代码
public ViewDataBinding getDataBinder(DataBindingComponent component, View[] views, int layoutId) {
    if (views != null && views.length != 0) {
        int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
        if (localizedLayoutId > 0) {
            Object tag = views[0].getTag();
            if (tag == null) {
                throw new RuntimeException("view must have a tag");
            }
        }
        // 代码缺失...
        return null;
    } else {
        return null;
    }
}

可以看到,这里的方法实现是缺失的,并不是我们期望看到的逻辑;那么期望的代码在哪里呢?我们先来看下 DataBinderMapper,这是一个抽象类;

csharp 复制代码
public abstract class DataBinderMapper {
    public abstract ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId);
    public abstract ViewDataBinding getDataBinder(DataBindingComponent bindingComponent,
            View[] view, int layoutId);
    public abstract int getLayoutId(String tag);
    public abstract String convertBrIdToString(int id);
    @NonNull
    public List<DataBinderMapper> collectDependencies() {
        // default implementation for backwards compatibility.
        return Collections.emptyList();
    }
}

我们点击它的子类实现,发现有三个同名的实现类:

apt 帮我们生成的 DataBinderMapperImpl 才是我们想要的类,我们进入这个类的 getDataBinder 看下:

csharp 复制代码
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);
        }
        throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
      }
    }
  }
  return null;
}

"layout/activity_main_0" 这个 tag 就是前面 DataBinding 帮我们创建的动态插入的 tag

是同一个 xml 文件,创建 ActivityMainBindingImpl 我们进入构造方法看下:

less 复制代码
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.EditText) bindings[1]
        , (android.widget.EditText) bindings[2]
        );
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.tv1.setTag(null);
    this.tv2.setTag(null);
    setRootTag(root);
    // listeners
    invalidateAll();
}

numBingdings 表示 xml 中的节点数量;

mapBindings 就是进行了 xml 的解析,同时将 xml 中的节点 view 封装到 databinding 中;

我们进入这个 mapBindings 方法看下,直接进入最终的 mapBindings 方法:

ini 复制代码
private static void mapBindings(DataBindingComponent bindingComponent, View view,
        Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
        boolean isRoot) {
    final int indexInIncludes;
    final ViewDataBinding existingBinding = getBinding(view);
    if (existingBinding != null) {
        return;
    }
    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('_');
        if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
            final int index = parseTagInt(tag, underscoreIndex + 1);
            if (bindings[index] == null) {
                bindings[index] = view;
            }
            indexInIncludes = includes == null ? -1 : index;
            isBound = true;
        } else {
            indexInIncludes = -1;
        }
    } 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;
        indexInIncludes = includes == null ? -1 : tagIndex;
    } else {
        // Not a bound view
        indexInIncludes = -1;
    }
    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);
            boolean isInclude = false;
            if (indexInIncludes >= 0 && child.getTag() instanceof String) {
                String childTag = (String) child.getTag();
                if (childTag.endsWith("_0") &&
                        childTag.startsWith("layout") && childTag.indexOf('/') > 0) {
                    // This *could* be an include. Test against the expected includes.
                    int includeIndex = findIncludeIndex(childTag, minInclude,
                            includes, indexInIncludes);
                    if (includeIndex >= 0) {
                        isInclude = true;
                        minInclude = includeIndex + 1;
                        final int index = includes.indexes[indexInIncludes][includeIndex];
                        final int layoutId = includes.layoutIds[indexInIncludes][includeIndex];
                        int lastMatchingIndex = findLastMatching(viewGroup, i);
                        if (lastMatchingIndex == i) {
                            bindings[index] = DataBindingUtil.bind(bindingComponent, child,
                                    layoutId);
                        } else {
                            final int includeCount =  lastMatchingIndex - i + 1;
                            final View[] included = new View[includeCount];
                            for (int j = 0; j < includeCount; j++) {
                                included[j] = viewGroup.getChildAt(i + j);
                            }
                            bindings[index] = DataBindingUtil.bind(bindingComponent, included,
                                    layoutId);
                            i += includeCount - 1;
                        }
                    }
                }
            }
            if (!isInclude) {
                mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
            }
        }
    }
}

本质就是:根据动态写入的 tag 解析每一个控件,并保存到 Object[] 数组中,并将这个数组传入到 ActivityMainBindingImpl 的构造方法中;

PS: 这个 Object[] 数组,是 DataBinding 耗费性能的一个点;

scss 复制代码
private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
    super(bindingComponent, root, 1
        , (android.widget.EditText) bindings[1]
        , (android.widget.EditText) bindings[2]
        );
    this.mboundView0 = (android.widget.LinearLayout) bindings[0];
    this.mboundView0.setTag(null);
    this.tv1.setTag(null);
    this.tv2.setTag(null);
    setRootTag(root);
    // listeners
    invalidateAll();
}

mboundView0 就是我们的根布局 LinearLayout, tv1、tv2 就是 TextView;

这也包含了 DataBinding 帮我们处理了 findViewById 的过程,本质是:通过遍历根 View(android.R.id.content) 下的所有子 View,存放到 Object[] 中,也就是这个数组中持有的是所有 View;

如何 setText 『数据(Model)变化如何更新 UI』

ActivityMainBindingImpl 会调用一行 super(bindingComponent, root, 1 , (android.widget.EditText) bindings[1] , (android.widget.EditText) bindings[2] ); 代码

这行代码,最终会调用的是 ViewDataBinding.java 这个类中;而这个类中,我们可以看到,有个静态代码块

java 复制代码
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) {
            }
        };
    }
}

OnAttachStateChangeListener 是一个监听,用来监听布局控件的变化,只要发生变化,就会执行这个 mRebindRunnable

如果界面上的控件很多,那么这里也是一个耗费性能的点; 我们进入这个 runnable 看下:

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

        if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
            if (!mRoot.isAttachedToWindow()) {
                mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                return;
            }
        }
        // 核心逻辑
        executePendingBindings();
    }
};

我们进入这个 executePendingBindings 看下:

csharp 复制代码
public void executePendingBindings() {
    if (mContainingBinding == null) {
        // 核心逻辑
        executeBindingsInternal();
    } else {
        mContainingBinding.executePendingBindings();
    }
}

最终会执行 executeBindingsInternal 这个方法,我们进入这个方法看下:

csharp 复制代码
private void executeBindingsInternal() {
    // ....
    if (!mRebindHalted) {
        // 核心逻辑
        executeBindings();
        if (mRebindCallbacks != null) {
            mRebindCallbacks.notifyCallbacks(this, REBOUND, null);
        }
    }
    mIsExecutingPendingBindings = false;
}

最终会执行这个 executeBindings 方法,这个方法是 ViewDataBinding 中的一个抽象方法,那么我们就要回到这个类的子类中去看,最终回到的就是 apt 帮我们生成的 ActivityMainBindingImpl.java 中

csharp 复制代码
protected void executeBindings() {
    // ....省略部分代码
    
    if ((dirtyFlags & 0xbL) != 0) {
        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv1, userName);
    }
    if ((dirtyFlags & 0xdL) != 0) {
        // 调用了 setText 设置内容
        androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tv2, userPwd);
    }
}

可以看到,更新 UI 的最终本质还是调用 setText 的逻辑;

总结:通过 ViewBinding.java 中声明的静态代码来监听页面中的每一个控件的变化,当数据发生变化的时候,执行一个 runnable,这个 runnable 中调用了 executeBindings 方法,最终调用到 每一个控件的 setText 方法来更新 UI;

View 变化如何更新数据(Model)

我们将 activity_main.xml 中的的 TextView 改成 EditeText 然后编译看下:executeBindings 方法中帮我们额外生成了一段代码:

kotlin 复制代码
if ((dirtyFlags & 0x8L) != 0) {
    androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.tv1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, tv1androidTextAttrChanged);
    androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.tv2, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, tv2androidTextAttrChanged);
}

给两个 EditeText 分别设置了 text 变动的监听:tv1androidTextAttrChanged tv2androidTextAttrChanged

ini 复制代码
private androidx.databinding.InverseBindingListener tv1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
    @Override
    public void onChange() {=
        java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(tv1);
        java.lang.String userName = null;
        boolean userJavaLangObjectNull = false;
        com.llc.databinding_java.User user = mUser;
        userJavaLangObjectNull = (user) != (null);
        if (userJavaLangObjectNull) {
            user.setName(((java.lang.String) (callbackArg_0)));
        }
    }
};

当 UI 变化的时候,触发 onChange 回调,调用 user.setName 给 model 赋值;

好了,DataBinding 就讲解到这里吧~

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力

相关推荐
alexhilton3 小时前
为什么你的App总是忘记所有事情
android·kotlin·android jetpack
雨白13 小时前
Jetpack系列(四):精通WorkManager,让后台任务不再失控
android·android jetpack
刘龙超1 天前
如何应对 Android 面试官 -> 玩转 JetPack ViewBinding
android jetpack
顾林海2 天前
ViewModel 销毁时机详解
android·面试·android jetpack
雨白2 天前
Jetpack系列(三):Room数据库——从增删改查到数据库平滑升级
android·android jetpack
雨白3 天前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
刘龙超4 天前
如何应对 Android 面试官 -> 玩转 JetPack LiveData
android jetpack
Wgllss14 天前
Kotlin+协程+FLow+Channel+Compose 实现一个直播多个弹幕效果
android·架构·android jetpack
_一条咸鱼_15 天前
Android Gson注解驱动的转换规则原理(9)
android·面试·android jetpack