如何应对 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 就讲解到这里吧~

欢迎三连


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

相关推荐
我命由我1234514 小时前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Jeled17 小时前
Android 本地存储方案深度解析:SharedPreferences、DataStore、MMKV 全面对比
android·前端·缓存·kotlin·android studio·android jetpack
我命由我123451 天前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton6 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i7 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户060905255229 天前
Compose 主题 MaterialTheme
android jetpack
用户060905255229 天前
Compose 简介和基础使用
android jetpack
用户060905255229 天前
Compose 重组优化
android jetpack
行墨9 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack