Android-MVVM-Databinding的原理、用法与封装

前言

说起 DataBinding/ViewBinding 的历史,可谓是好事多磨,乃至是比 Dagger/Hilt 还要传奇。

说起依赖注入结构 Dagger2/Hilt ,也是比较传奇,刚出来的时分火的一塌糊涂,各种攻略教程,随后发现坑多难以运用,随之逐渐预冷,近几年在 Hilt 发布之后越发的火爆了。

而 DataBinding/ViewBinding 作为 Android 官方的亲儿子库,它的阅历却愈加的古怪,从发布的时分火爆,然后到坑太多直接遇冷,随之被其他结构替代,再到后面 Kotlin 出来之后是愈加的冷门了,全网是一片吐槽,跟着 Kotlin 插件抛弃之后 ViewBinding 的推出而再度翻火…都够拍一部大片了。

提到这儿了,在Android开发者,特别是没用过 DataBinding 的开发者心中或许都有一个大致的形象,DataBinding太坑了,太老了,更新慢,都是缺陷,跑都跑不起来,狗都不必…

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

这也是 DataBinding/ViewBinding 结构的开展历程导致的,几起几落结果就给开发者留下了满是缺陷这么个形象。

那么作为官方主推的 MVVM 架构指定结构 DataBinding 真的有这么不堪吗?

在现在看来 Android 客户端开发还没有进化到 Compose,咱们现在的干流布局方案仍是XML,而根据VMMV架构的 DataBinding 结构仍是很有必要学习与运用的。

老话这么说,我能够不必,可是我要会。就算自己不必,至少也要能看懂别人的代码吧。

闲话不多说,下面就简略从几点剖析一下,为什么Googel引荐运用 DataBinding/ViewBinding ,怎么运用,以及根本的原理,最后引荐一些 DataBinding 的封装简化运用流程。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

一、之前的方案有哪些不足

只要是 Android 开发的从业者,从开端学习起就知道找控件的办法是 findViewById,下面先讲讲它的大致原理。

咱们以Activity中运用 findViewById 为例:

androidx.appcompat.app

    @Override
    public <T extends View> T findViewById(@IdRes int id) {
        return getDelegate().findViewById(id);
    }

能够看到是经过派遣类调用的,其实是调用到 Window 类中的 findViewById 办法:

    public <T extends View> T findViewById(int id) {
        if (id == NO_ID) {
        return null;
        }
        return findViewTraversal(id);
    }

内部又调用到 ViewGroup 的 findViewTraversal 办法。内部又是遍历找 id 的逻辑

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

假如布局正好在此 ViewGroup 中那只遍历一次,假如嵌套的很深,则会一层一层的遍历去找 id ,这是会稍稍影响功用的。

而且咱们在运用 findViewById 的时分是或许呈现的过错问题:

  1. 需求强转的问题。
  2. 调用机遇过错的问题。
  3. 呼应式布局中因为布局差异导致空指针的问题。
  4. Activity+Fragment架构中,Fragment初始化了可是没有增加到Activity中导致的问题。
  5. 假如一个Activity中有多个Fragment,Fragment中的控件名称又有重复的,直接运用findViewById会爆错。
  6. 同样的问题再Dialog与PopuoWindow都或许存在已初始化但没增加的问题。
  7. 当前Activity找到其他Activity的相同id,但实在不存在的问题。
  8. 因为重建、康复导致的控件空指针问题。

等等,当然了,其中许多问题是逻辑问题导致的空指针,锅不能都扣到 findViewById 头上。就算咱们运用其他的包含 DataBinding 的方案时也并不能完全避免空指针的,只能说尽量避免空指针。

这都不说了,关键是当布局中的 ID 许多的时分,需求写大量的 findViewById 模板代码。这简直是要命了,所以就引申出了许多结构或插件。

例如 XUtils,ButterKnife,FindViewByMe(插件)等。

尽管 XUtils,ButterKnife 这类插件能够专门对 findviewbyid 办法进行简化,可是仍是需求写注解让控件与资源绑定,当然后期还专门有针对绑定的插件。

可是其本质仍是 findViewById 那一套,再后来跟着组件化与插件化的炽热,相似 ButterKnife 在这样的架构中或多或少的有一些其他的问题 R R1 R2…总感觉乖乖的,有点鸡肋的意思,用的人也是越来越少了。

而跟着 Kotlin 的流行,和 kotlin-android-extensions 插件的诞生,全部又不相同了,开发者也有了新的挑选。

Kotlin 直接从言语层面支撑 Null 安全,所以 DataBinding 在 Kotlin 言语的项目中根本上是隐姓埋名了。

许多人或许便是因为 kotlin-android-extensions 插件然后运用 Kotlin 的,不需求手动 findviewbyid 了,实在是太爽了。

kotlin-android-extensions 是怎么结束的,咱们检查一下 Kotlin Bytecode 的字节码:

public final class MainActivity extends AppCompatActivity {
   private HashMap _$_findViewCache;
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(1300023);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.textView);
      var10000.setText((CharSequence)"Hello");
   }
   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }
      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }
      return var2;
   }
}

kotlin-android-extensions插件会帮咱们生成一个_$_findCachedViewById()函数,优先从内存缓存 HashMap 中找控件,找不到就会调用原生的 findViewById 增加到内存缓存中,是的,便是咱们常用的很简略的缓存逻辑。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

后期的开展大家也知道了,跟着 apply plugin: 'kotlin-android-extensions' 插件被官方背离了,至于为什么被抛弃,我个人大致猜想或许是:

  1. 底层仍是根据 findViewById,仍是会有 findViewById 的坏处,仅仅多了缓存的处理。
  2. 就算是多了缓存看起来很美,但缓存并不好用,在部分需求收回再次运用的场景,例如 RV.Adapter.ViewHolder 中存在缓存失效每次都 findViewById 而导致的功用问题(还不如不要呢)。
  3. 每一个 Page/Item 都需求一个 HashMap 来保存 View 实例,占用内存过大。
  4. xml 中的 ID 没有跟页面绑定,相同有 findViewById 的那些问题,在当前 Activity 能够找到其他页面的 ID。

再而后 2019 年 Google 推出了 ViewBinding 终结全部,假如布局中的某个 View 实例隐含 Null 安全隐患,则编译时 ViewBinding 中间代码为其生成 @Nullable 注解。然后最大极限避免控件的空指针反常。而且因为视图绑定会创立对视图的直接引证,因而不存在因视图的 ID 无效而引发空指针反常。而且每个绑定类中的字段均具有与它们在 xml 文件中引证的视图相匹配的类型。这意味着不存在发生类转化反常的风险。

而 DataBinding 作为 ViewBinding 的老大哥则又一次登上了舞台。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

**DataBinding VS ViewBinding :**两者都能做 binding UI layouts 的操作,可是 DataBinding 还支撑一些额定的功用,如双向绑定,xml中运用变量等。ViewBinding不会增加编译时刻,而 DataBinding 会增加编译时刻,而且 DataBinding 会少量增加 apk 体积, ViewBinding 不会。总的来说ViewBinding愈加的轻量。

题外话:ButterKnife 的作者现已宣布不保护 ButterKnife,作者引荐运用 ViewBinding 了。

二、ViewBinding/DataBinding怎么运用

因为 DataBinding 是与 AGP(Android Gradle 插件) 捆绑在一起的,所以咱们不需求导依赖包,只需求在装备中发动即可。

老版别界说如下(4.0版别以下):

android {
    viewBinding {
        enabled = true
    }
    dataBinding{
        enabled = true
    }
}

新版别界说如下(4.0版别以上):

android {
    buildFeatures {
        dataBinding = true
        viewBinding = true
    }
}

装备结束之后在咱们的xml根布局标签上 alt + enter,就能够提示转化为 DataBindingLayout了。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

转化结束便是这样:

<?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:binding="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="RtlHardcoded">
    <data>
    </data>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        tools:viewBindingIgnore="true">
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/splash_center_blue_logo" />
    </FrameLayout>
</layout>

能够看到多了一个data的标签,咱们就能够在data中界说变量与变量的类型。

    <data>
        <import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <import type="android.text.TextUtils"/>
        <variable name="list" type="List&lt;String&gt;"/>
        <variable name="sparse" type="SparseArray&lt;String&gt;"/>
        <variable name="map" type="Map&lt;String, String&gt;"/>
        <variable name="index" type="int"/>
        <variable name="key" type="String"/>
    </data>

import 是界说导入需求的类,variable是界说需求的变量是由外部传入,咱们能够运用多种办法传入界说的variable目标。

例如:

    <data>
        <variable
            name="viewModel"
            type="com.hongyegroup.cpt_auth.mvvm.vm.UserLoginViewModel" />
        <variable
            name="click"
            type="com.hongyegroup.cpt_auth.ui.UserLoginActivity.ClickProxy" />
        <import type="com.guadou.lib_baselib.utils.NumberUtils" />
    </data>

运用起来如下:

    <TextView
        android:id="@+id/tv_get_code"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="@dimen/d_15dp"
        android:background="@{NumberUtils.isStartWithNumber(viewModel.mCountDownLD)?@drawable/shape_gray_round7:@drawable/shape_white_round7}"
        android:enabled="@{!NumberUtils.isStartWithNumber(viewModel.mCountDownLD)}"
        android:paddingLeft="@dimen/d_12dp"
        android:paddingTop="@dimen/d_5dp"
        android:paddingRight="@dimen/d_12dp"
        android:paddingBottom="@dimen/d_5dp"
        android:text="@={viewModel.mCountDownLD}"
        android:textColor="@{NumberUtils.isStartWithNumber(viewModel.mCountDownLD)?@color/white:@color/light_blue_text}"
        android:textSize="@dimen/d_13sp"
        binding:clicks="@{click.getVerifyCode}"
        tools:background="@drawable/shape_white_round7"
        tools:text="Get Code"
        tools:textColor="@color/light_blue_text" />

页面的数据都保存在ViewModel中,页面的事情都封装在Click目标中,还能经过NumberUtils直接运用内部的办法了。

在Activity中就能够绑定 Activity 与 DataBinding 了,代码如下:

class MainActivity : AppCompatActivity() {
  private lateinit var mainBinding: ActivityMainBinding
  private lateinit var mainViewModel: MainViewModel by viewModels()
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
      mainBinding.lifecycleOwner = viewLifecycleOwner
        //设置变量(更容易了解)
        mBinding.setVariable(BR.viewModel,mainViewModel)
        //设置变量(更便利)
    mainBinding.viewModel = mainViewModel
   }
}

其中 ActivityMainBinding 这个类便是系统生成的,生成规则是布局文件名称转化为驼峰大小写办法,然后在末尾增加 Binding 后缀。如 activity_main 编译为 ActivityMainBinding 。

现在的绑定比刚开端的 DataBinding 真的现已便利许多了。而 Fragment 的绑定有些许不同。

class MainFragment : Fragment() {
  private lateinit var mainBinding: FragmentMainBinding
  private lateinit var mainViewModel: MainViewModel by viewModels()
   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return setContentView(container)
    }
    fun setContentView(container: ViewGroup?): View {
        mainBinding = DataBindingUtil.inflate<ActivityMainBinding>(layoutInflater, R.layout.fragment_main, container, false)
        mainBinding.lifecycleOwner = viewLifecycleOwner
        //设置变量(更容易了解)
        mBinding.setVariable(BR.viewModel,mainViewModel)
        //设置变量(更便利)
    mainBinding.viewModel = mainViewModel
        return mBinding.root
    }
}

怎么在xml运用变量呢?

集合的运用:


android:text="@{list[index]}"
android:text="@{sparse[index]}"
android:text="@{map[key]}"

文本的运用:

android:text="@{user.firstName, default=PLACEHOLDER}"
//常用的三元与判空
android:text="@{user.name != null ? user.name : user.nickName}"
android:text="@{user.name ?? user.nickName}"
android:visibility="@{user.active ? View.VISIBLE : View.GONE}"

事情的简略处理:

android:onClick="@{click::onClickFriend}"
android:onClick="@{() -> click.onSaveClick(task)}"
android:onClick="@{(theView) -> click.onSaveClick(theView, task)}"
android:onLongClick="@{(theView) -> click.onLongClick(theView, task)}"
//控件隐藏不设置点击,显现才设置点击
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"

双向绑定:@= 与 @ 的区别

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@={click.etLiveData}" />
    <Textview
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@{click.etLiveData}" />    

运用单向绑定的时分@{},viewModel中的数据改动了,就会影响到TextView的显现。而双向绑定则是当EditText内部的文本发生改动了也同样会影响到viewModel中的数据改动。

三、DataBinding的进阶运用

关于 DataBinding 的根底运用,信任大家或多或少都有看过或许用过,知道根底运用就能在开发中实际开发了吗?太年轻了!

详细用过 DataBinding 的或多或少都遇到过一些坑,作为一个常年运用 DataBinding 的开发者,我对下面几点实际开发中遇到的一些形象深入的知识点做一些实用的引申。

3.1 RV.Adapter中运用

与 Fragment 的运用办法相似,咱们只需求绑定了 View 之后设置给ViewHodler即可。

class UserAdapter(users: MutableList<User>, context: Context) :
  RecyclerView.Adapter<UserAdapter.MyHolder>() {
  class MyHolder(val binding: TextItemBinding) : RecyclerView.ViewHolder(binding.root)
​
  private var users: MutableList<User> = arrayListOf()
  private var context: Context
​
  init {
    this.users = users
    this.context = context
   }
​
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
    val inflater = LayoutInflater.from(context)
    val binding: TextItemBinding = DataBindingUtil.inflate(inflater, R.layout.text_item, parent, false)
    return MyHolder(binding)
   }
​
  override fun onBindViewHolder(holder: MyHolder, position: Int) {
    holder.binding.user = users[position]
    holder.binding.executePendingBindings()
   }
  override fun getItemCount() = users.size
}

3.2 自界说View的运用

比方我界说一个自界说View,在内部运用了自界说的特点,需求在 xml 中赋值,

<com.guadou.kt_demo.demo.demo12_databinding_texing.CustomTestView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    binding:clickProxy="@{click}"
    binding:testBean="@{testBean}" />

咱们再自界说View的类中就能够经过 setXX 拿到这个赋值的特点了。

class CustomTestView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    LinearLayout(context, attrs, defStyleAttr) {
    init {
        orientation = VERTICAL
        //传统的办法增加
        val view = CommUtils.inflate(R.layout.layout_custom_databinding_test)
        addView(view)
    }
    //设置特点
    fun setTestBean(bean: TestBindingBean?) {
        bean?.let {
            findViewById<TextView>(R.id.tv_custom_test1).text = it.text1
            findViewById<TextView>(R.id.tv_custom_test2).text = it.text2
            findViewById<TextView>(R.id.tv_custom_test3).text = it.text3
        }
    }
    fun setClickProxy(click: Demo12Activity.ClickProxy?) {
        findViewById<TextView>(R.id.tv_custom_test1).click {
            click?.testToast()
        }
    }
}

假如咱们的自界说View不是写在 XML 中,而是经过Java代码手动 add 到布局中,相同的能够经过 new 目标,设置自界说特点来结束相同的作用:

    //给静态的xml,赋值数据,赋值结束之后 include的布局也能够自动显现
    mBinding.testBean = TestBindingBean("haha2", "heihei2", "huhu2")
    //动态的增加自界说View
    val customTestView = CustomTestView(mActivity)
    customTestView.setClickProxy(clickProxy)
    customTestView.setTestBean(TestBindingBean("haha3", "heihei3", "huhu3"))
    mBinding.flContent.addView(customTestView)

3.3 include与viewStub的运用

include 和 viewStub 的用法差不多,这儿以 include 为例:

例如咱们在 Activity 的 xml 布局中增加一个 include 的布局。

<?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:binding="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="RtlHardcoded">
    <data>
        <variable
            name="testBean"
            type="com.xx.xx.demo.TestBindingBean" /> 
    </data>
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/white"
        tools:viewBindingIgnore="true">
      ...
         <include
            layout="@layout/include_databinding_test"
            binding:click="@{click}"
            binding:testBean="@{testBean}" />
    </FrameLayout>
</layout>

咱们能够直接把 Activity 的自界说特点 testBean 传入到 include 布局中。

include_databinding_test:

<?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:binding="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="testBean"
            type="com.guadou.kt_demo.demo.demo12_databinding_texing.TestBindingBean" />
        <import
            alias="textUtlis"
            type="android.text.TextUtils" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:layout_marginTop="15dp"
            android:text="下面是赋值的数据"
            binding:clicks="@{click.testToast}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{testBean.text1}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{testBean.text2}" />
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{testBean.text3}" />
    </LinearLayout>
</layout>

这样在 include 的 xml 中能直接运用自界说特点来显现了。

而假如动态的 inflate 布局就和自界说 View 的处理办法相似了:

    mBinding.testBean = TestBindingBean("haha", "heihei", "huhu")
    //获取View
    val view = CommUtils.inflate(R.layout.include_databinding_test)
    //绑定DataBinding 并赋值自界说的数据
    DataBindingUtil.bind<IncludeDatabindingTestBinding>(view)?.apply {
        testBean = TestBindingBean("haha1", "heihei1", "huhu1")
    }
    //增加布局
    mBinding.flContent.addView(view)    

3.4 自界说事情与特点

要点便是自界说的特点与事情处理了,一些喜爱在 xml 中写逻辑的都是根据此办法结束的,下面一起看看怎么运用自界说特点:

Java言语的结束:

public class BindingAdapter {
    @android.databinding.BindingAdapter("url")
    public static void setImageUrl(ImageView imageView, String url) {
        Glide.with(imageView.getContext())
                .load(url)
                .into(imageView);
    }
}

办法名不是关键,关键的是注解上面的值 “url”,才是在xml中显现的自界说特点,而办法中的参数,第一个是限制在哪一个控件上生效的,是固定的比传的参数,而第二个参数 String url 才是咱们自界说传入的参数。

这个比如很简略,便是传入url,在 ImageView 上经过 Glide 显现图片。

用Kotlin的办法结束就更简略了:

@BindingAdapter("url")
fun setImageUrl(view: ImageView, url: String?) {
    if (!url.isNullOrEmpty()) {
        Glide.with(view.context)
                .load(imageUrl)
                .into(view)
    }
}

或许运用Kotlin的顶层扩展函数也能结束:

@BindingAdapter("url")
fun ImageView.setImageUrl(url: String?) {
     if (!url.isNullOrEmpty()) {
        Glide.with(view.context)
                .load(imageUrl)
                .into(this)
    }
}

三种界说的办法都是相同的,除此之外,咱们除了加一个参数,咱们还能参与多个参数,乃至还能指定可选参数和必填参数:

  @android.databinding.BindingAdapter(value = {"imgUrl", "placeholder"}, requireAll = false)
    public static void loadImg(ImageView imageView, String url, Drawable placeholder) {
        GlideApp.with(imageView)
                .load(url)
                .placeholder(placeholder)
                .into(imageView);
    }

运用:

    <ImageView
        android:id="@+id/img_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        binding:imgUrl="@{user.url}"
        binding:placeholder="@{@drawable/ic_launcher_background}"
    />

这儿 requireAll = false 表明咱们能够运用这两个两个特点中的任一个或一起运用,假如 requireAll = true 则两个特点必须一起运用,不然会在编译器报错,现在也 AS 会明确的指出过错地便利利修正的。

3.5 自界说转化器

Converters 转化器其实是用的比较少,可是在一些特别的场景有奇效,特别是做一些多主题,国际化的时分。

<Button
    android:onClick="toggleIsError"
    android:text="@{isError ? @color/red : @color/white}"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

这样就能够根据颜色来显现不同的文本:

@BindingConversion
public static int convertColorToString(int color) {
    switch (color) {
        case Color.RED:
            return R.string.red;
        case Color.WHITE:
            return R.string.white;
    }
    return R.string.black;
}

3.6 DataBinding中字符串的各种特别处理

假如说 DataBinding 用的最多的控件,那必定是 TextView ,而文本的显现有多样的办法,国际化、占位符、Html/Span等多样的文本怎么在 DataBinding 的 xml 中展现又是一个新的问题。

经过前面的根本运用和部分高级的运用,这儿就直接放代码了。

1. databinding运用string format 占位符:

<string name="Generic_Text">My Name is %s</string>
android:text= "@{@string/Generic_Text(Profile.name)}"

当然也能够直接运用字符串的,可是外面的一层要用单引号

 android:text='@{viewModel.mHoldAccount,default="22"}'

2. 运用Html标签

<![CDATA[<font color=\'#FF9900\'>作品阅览次数<font color=\'#333333\'> %1$s </font>次</font>]]>
<data>
    <import type="android.text.Html"/>
</data>
...
 android:text="@{Html.fromHtml(@string/sxx_user_rank(user.readTimes))}"

3.Html中运用三元表达式

过错办法:

android:text="@{task.title_total>0?Html.fromHtml(@string/task_title(task.title,task.title_num,task.title_total)):task.title}"

正确办法:

android:text="@{Html.fromHtml(task.title_total>0?@string/task_title(task.title,task.title_num,task.title_total):task.title)}"

4.default的结束

相似tools的结束:

android:text="@{viewModel.mYYPayLiveData.reward_points,default=@string/normal_empty}"

等同于:

android:text="@{viewModel.mYYPayLiveData.reward_points}"
tools:text="@string/normal_empty"

相似hilt的结束:

binding:text="@{viewModel.mSelectBankName}"
binding:default="@{@string/normal_empty}"
tools:text="@string/normal_empty"

运用自界说特点结束:

@BindingAdapter("text", "default", requireAll = false)
fun setText(view: TextView, text: CharSequence?, default: String?) {
    if (text == null || text.trim() == "" || text.contains("null")) {
        view.text = default
    } else {
        view.text = text
    }
}

四、DataBinding的简略原理

ViewBinding的生成过程,便是一系列处理 Tag 的逻辑。将布局中的含有databinding赋值的 Tag 控件存入bindings的Object的数组中并返回。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

在 ActivityMainBindingImpl 生成类中该办法中将获取的 View 数组赋值给成员变量。(相比 findViewById 只遍历了一次)

DataBinding 经过布局中的 Tag 将控件查找出来,然后根据生成的装备文件进行对应的同步操作,设置一个大局的布局改动监听来实时更新,经过他的set办法进行同步。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

所以咱们才说 DataBinding 不参与视图逻辑,仅担任通知结束 View 状况改动,仅用于规避 Null 安全问题。

总的来说,DataBinding 的原理没有什么黑科技,便是是根据数据绑定和观察者模式的。它经过生成代码来结束UI组件和数据目标之间的绑定,并运用观察者模式来坚持UI和数据之间的同步。

五、简化DataBinding的运用(封装)

或许有同学看了根本的运用和一些进阶的运用之后,更坚定了心中的想法,可去你的吧,运用这么麻烦,狗都不必…

别急,咱们还能对一些固定的场景化的用法做一些封装嘛,反正常用的几种办法,有限并不包含于一些字符串处理,图片处理,数据适配器的处理,UI的处理等一些办法界说好了或许封装好了运用起来便是so easy!

5.1 Activity/Fragment主页面封装

一般关于Activity/Fragment 咱们主要是封装的 DataBinding 与 ViewModel。

不同的人有不同的封装办法,有的用泛型+传参的办法,有的用泛型+反射的办法,有的封装了 DataBinding 的填充自界说特点逻辑。

下面别离演示不同的封装办法:

abstract class BaseVDBActivity<VM : ViewModel,VB : ViewBinding>(
   private val vmClass: Class<VM>, private val vb: (LayoutInflater) -> VB,
) : AppCompatActivity() {
    //因为传入了参数,能够直接构建ViewModel
    protected val mViewModel: VM by lazy {
        ViewModelProvider(viewModelStore, defaultViewModelProviderFactory).get(vmClass)
    }
    //假如运用DataBinding,自己再赋值
}

这种办法运用了泛型+传参,运用的时分需求填入构造参数:

class MainActivity : BaseVDBActivity<ActivityMainBinding, MainViewModel>(
    ActivityMainBinding::inflate,
    MainViewModel::class.java
) {
    //就能够直接运用ViewBinding与ViewModel 
    fun test() {
        mBinding.iconIv.visibility = View.VISIBLE
        mViewModel.data1.observe(this) {
        }
    }
}

假如是运用的 DataBinding,咱们还能把 DataBinding 的特点赋值逻辑进行封装:

封装一个Config目标

class DataBindingConfig(
    private val layout: Int,
    private val vmVariableId: Int,
    private val stateViewModel: BaseViewModel
) {
    private var bindingParams: SparseArray<Any> = SparseArray()
    fun getLayout(): Int = layout
    fun getVmVariableId(): Int = vmVariableId
    fun getStateViewModel(): BaseViewModel = stateViewModel
    fun getBindingParams(): SparseArray<Any> = bindingParams
    fun addBindingParams(variableId: Int, objezt: Any): DataBindingConfig {
        if (bindingParams.get(variableId) == null) {
            bindingParams.put(variableId, objezt)
        }
        return this
    }
}

运用 Config 目标给 DataBinding 赋值自界说特点的封装:

abstract class BaseVDBActivity<VM : BaseViewModel, VDB : ViewDataBinding> : BaseVMActivity<VM>() {
    protected lateinit var mBinding: VDB
    protected abstract fun getDataBindingConfig(): DataBindingConfig
    override fun getLayoutRes(): Int = -1
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = DataBindingUtil.setContentView(this, getDataBindingConfig().getLayout())
        mBinding.lifecycleOwner = this
        mBinding.setVariable(
            getDataBindingConfig().getVmVariableId(),
            getDataBindingConfig().getStateViewModel()
        )
        val bindingParams = getDataBindingConfig().getBindingParams()
        bindingParams.forEach { key, value ->
            mBinding.setVariable(key, value)
        }
        init(savedInstanceState)
    }
}

Fragment的封装也是迥然不同:

abstract class BaseVDBFragment<VM : BaseViewModel, VDB : ViewDataBinding> : BaseVMFragment<VM>() {
    protected lateinit var mBinding: VDB
    override fun getLayoutRes(): Int = -1
    protected abstract fun getDataBindingConfig(): DataBindingConfig
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding =
            DataBindingUtil.inflate(inflater, getDataBindingConfig().getLayout(), container, false)
        mBinding.lifecycleOwner = viewLifecycleOwner
        mBinding.setVariable(
            getDataBindingConfig().getVmVariableId(),
            getDataBindingConfig().getStateViewModel()
        )
        val bindingParams = getDataBindingConfig().getBindingParams()
        bindingParams.forEach { key, value ->
            mBinding.setVariable(key, value)
        }
        return mBinding.root
    }
}

咱们运用的时分就直接赋值自界说特点:

class ProfileFragment : BaseFragment<ProfileViewModel, FragmentProfileBinding>() {
    override fun getDataBindingConfig(): DataBindingConfig {
        return DataBindingConfig(R.layout.fragment_profile, BR.viewModel, mViewModel)
            .addBindingParams(BR.click, ClickProxy())
    }
    private val articleAdapter by lazy { ArticleAdapter(requireContext()) }
    ...
}

详细的代码太多了,能够参照文章结束的项目。

5.2 RV.Adapter的封装

其实在之前的 RV.Adapter 运用中,咱们也能根据这个 Adapter 封装,可是咱们项目中运用的仍是BRVAH,所以咱们就根据此封装的。

open class BaseBindAdapter<T>(layoutResId: Int, br: Int)
    : BaseQuickAdapter<T, BaseBindAdapter.BindViewHolder>(layoutResId) {
    private val _br: Int = br
    override fun convert(helper: BindViewHolder, item: T) {
        helper.binding.run {
            setVariable(_br, item)
            executePendingBindings()
        }
    }
    override fun getItemView(layoutResId: Int, parent: ViewGroup?): View {
        val binding = DataBindingUtil.inflate<ViewDataBinding>(mLayoutInflater, layoutResId, parent, false)
                ?: return super.getItemView(layoutResId, parent)
        return binding.root.apply {
            setTag(R.id.BaseQuickAdapter_databinding_support, binding)
        }
    }
    class BindViewHolder(view: View) : BaseViewHolder(view) {
        val binding: ViewDataBinding
            get() = itemView.getTag(R.id.BaseQuickAdapter_databinding_support) as ViewDataBinding
    }
}

运用的时分,能够挑选继承这个基类结束:

class HomeArticleAdapter(layoutResId: Int = R.layout.item_article_constraint) :
        BaseBindAdapter<Article>(layoutResId, BR.article) {
    override fun convert(helper: BindViewHolder, item: Article) {
        super.convert(helper, item)
        helper.addOnClickListener(R.id.articleStar)
        helper.setImageResource(R.id.articleStar, if (item.collect) R.drawable.timeline_like_pressed else R.drawable.timeline_like_normal)
        else helper.setVisible(R.id.articleStar, false)
        helper.setText(R.id.articleAuthor,if (item.author.isBlank()) "分享者: ${item.shareUser}" else item.author)
        Timer.stop(APP_START)
    }
}

乃至在一些简略的布局展现逻辑,咱们都无需继承基类结束,直接:

 private val systemAdapter by lazy { BaseBindAdapter<SystemParent>(R.layout.item_system, BR.systemParent) }

5.3 常用的自界说特点与事情作用

EditText:

/**
 * EditText的简略监听事情
 */
@BindingAdapter("onTextChanged")
fun EditText.onTextChanged(action: (String) -> Unit) {
    addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
        }
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            action(s.toString())
        }
    })
}
var _viewClickFlag = false
var _clickRunnable = Runnable { _viewClickFlag = false }
/**
 * Edit的确认按键事情
 */
@BindingAdapter("onKeyEnter")
fun EditText.onKeyEnter(action: () -> Unit) {
    setOnKeyListener { _, keyCode, _ ->
        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            KeyboardUtils.closeSoftKeyboard(this)
            if (!_viewClickFlag) {
                _viewClickFlag = true
                action()
            }
            removeCallbacks(_clickRunnable)
            postDelayed(_clickRunnable, 1000)
        }
        return@setOnKeyListener false
    }
}
/**
 * Edit的失去焦点监听
 */
@BindingAdapter("onFocusLose")
fun EditText.onFocusLose(action: (textView: TextView) -> Unit) {
    setOnFocusChangeListener { _, hasFocus ->
        if (!hasFocus) {
            action(this)
        }
    }
}
/**
 * 设置ET小数点2位
 */
@BindingAdapter("setDecimalPoints")
fun setDecimalPoints(editText: EditText, num: Int) {
    editText.filters = arrayOf<InputFilter>(ETMoneyValueFilter(num))
}

ImageView:

/**
 * 设置图片的加载
 */
@BindingAdapter("imgUrl", "placeholder", "isOriginal", "roundRadius", "isCircle", requireAll = false)
fun loadImg(
    view: ImageView, url: Any?, placeholder: Drawable? = null, isOriginal: Boolean = false, roundRadius: Int = 0,
    isCircle: Boolean = false
) {
    url?.let {
        view.extLoad(
            it,
            placeholder = placeholder,
            roundRadius = CommUtils.dip2px(roundRadius),
            isCircle = isCircle,
            isForceOriginalSize = isOriginal
        )
    }
}
@BindingAdapter("loadBitmap")
fun loadBitmap(view: ImageView, bitmap: Bitmap?) {
    view.setImageBitmap(bitmap)
}

TextView:

//为空的时分设置默认值
@BindingAdapter("text", "default", requireAll = false)
fun setText(view: TextView, text: CharSequence?, default: String?) {
    if (text == null || text.trim() == "" || text.contains("null")) {
        view.text = default
    } else {
        view.text = text
    }
}
//设置Html字体
@BindingAdapter("textHtml")
fun setTextHtml(textView: TextView, text: String?) {
    if (!TextUtils.isEmpty(text)) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            textView.text = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)
        } else {
            textView.text = Html.fromHtml(text)
        }
    } else {
        textView.text = ""
    }
}
/**
 * 设置左右的Drawable图标
 */
@BindingAdapter("setRightDrawable")
fun setRightDrawable(textView: TextView, drawable: Drawable?) {
    if (drawable == null) {
        textView.setCompoundDrawables(null, null, null, null)
    } else {
        drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight)
        textView.setCompoundDrawables(null, null, drawable, null)
    }
}
@BindingAdapter("setLeftDrawable")
fun setLeftDrawable(textView: TextView, drawable: Drawable?) {
    if (drawable == null) {
        textView.setCompoundDrawables(null, null, null, null)
    } else {
        drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight)
        textView.setCompoundDrawables(drawable, null, null, null)
    }
}

View:

/**
 * 设置控件的隐藏与显现
 */
@BindingAdapter("isVisibleGone")
fun isVisibleGone(view: View, isVisible: Boolean) {
    view.visibility = if (isVisible) View.VISIBLE else View.GONE
}
@BindingAdapter("isInVisibleShow")
fun isInVisible(view: View, isVisible: Boolean) {
    view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
}
/**
 * 点击事情防抖动的点击
 */
@BindingAdapter("clicks")
fun clicks(view: View, action: () -> Unit) {
    view.click { action() }
}
/**
 * 重新设置高度
 */
@BindingAdapter("layoutHeight")
fun layoutHeight(view: View, targetHeight: Float) {
    val height = view.layoutParams.height
    if (height != targetHeight.toInt()) {
        view.apply {
            this.layoutParams = layoutParams.apply {
                this.height = targetHeight.toInt()
            }
        }
    }
}
//设置动画设置高度
@SuppressLint("Recycle")
@BindingAdapter("layoutHeightAnim")
fun layoutHeightAnim(view: View, targetHeight: Float) {
    val layoutParams = view.layoutParams
    val height = layoutParams.height
    if (height != targetHeight.toInt()) {
        //值的特点动画
        val animator = ValueAnimator.ofInt(height, targetHeight.toInt()).apply {
            addUpdateListener {
                val heightVal = it.animatedValue as Int
                layoutParams.height = heightVal
                view.layoutParams = layoutParams
            }
            duration = 250
        }
        //不能再子线程中更新UI,假如是其他的值是能够的比方Tag
        AsyncAnimUtil.instance.startAnim(view.findViewTreeLifecycleOwner(), animator, false)
    }
}

因为篇幅原因只贴出了自用的相对重要的部分,假如想要检查完好的能够去文章末尾检查源码展现。

总结

DataBinding 比照 findviewbyid 比照的优缺陷:

长处:

  1. 简化 findviewbyid 模板代码,更简练易懂。
  2. 支撑双向绑定与单向绑定,可选可装备,更灵活。
  3. xml布局与页面的一一对应,尽量削减空指针反常,合作 Kotlin 的非空校验更舒适。
  4. 经过生成的绑定类削减代码执行时刻,内部还注册目标的懒加载,能够带来必定的功用优化。
  5. 便利做换肤与国际化,能够经过适配器更精细的操作样式与文本。

缺陷:

  1. 兼容性问题(晋级AS版别与Gradle版别)
  2. 不便利调试(再次引荐不要在XML里写逻辑,而且现在AS晋级后现已能明确指出大部分的问题)
  3. 编译时刻更长了(特别是第一次需求生成许多的Bind类文件,再次运行有缓存和增量更新会好一点)
  4. 少量增加APK体积(究竟多了许多类)

运用DataBinding的一些小Tips:

1.想用双向绑定就用,不想用双向绑定就用单向绑定,都不想用只用findviewbyid也是能够的。完全看大家的喜爱,当然不必DataBinding/ViewBinding 也行的,能够用其他的结构或许原生的findviewbyid都行的。

2.假如要发动 DataBinding ,引荐你顺便加上 ViewBinding

buildFeatures {
  viewBinding = true
  dataBinding = true
}

DataBinding是 ViewBinding 的超集,假如只想替换findviewbyid的功用,那你能够运用运用 ViewBinding ,假如想强制指定不生成 ViewBinding 编译文件,能够加上tools:viewBindingIgnore="true"

3.DataBinding尽管支撑能够在xml里边写复杂的核算逻辑,但仍是引荐大家尽量只做数据的绑定,逻辑核算尽量不要卸载xml里边,假如真要写逻辑,最多只做三元的逻辑判断。以免呈现一些功用问题与调试问题。

4.DataBinding合作ViewModel和LiveData食用更舒适,能够绑定生命周期也引荐大家要绑定到lifecycleOwner,它能够自动毁掉资源,在此场景中 Flow 反而没有 LiveData 好用,而且在部分版别中 LiveData 反而兼容性更好。

5.xml 的标签尽量把自界说特点的 app 标签与 DataBinding 标签 databinding 区分开来便于后期的保护和搭档的协同开发。

6.善用 BindingAdapter 进行数据绑定与设置监听。

总的来说用仍是不必 DataBinding 还真是存乎一心,都行,仅仅我个人觉得在当下这个时刻点看的话是利大于弊。再往后我也不好说,究竟 Compose 把整个 xml 体系都给革命了。

提到这儿请容许我挣扎一下先给自己叠个甲:

我认为原生 Android 的未来必定是 Compose ,可是多少年之后能走向干流不好说,3年?5年?究竟 Kotlin 言语推出到今年这么多年了也只和 Java 55开而已,乃至我认识的很多5年以上的开发者都没用过 Kotlin,反而现在干流的 MVVM 中仍是许多是运用 DataBinding 的,就算咱们不必也是需求了解的。

或许真的有许多人对 DataBinding 不喜爱、不感冒,也能了解。其实我也是各种机缘巧合下才入的坑,我也是从开端的厌弃,到真香,再抛弃,最后一向运用至今。

没有最好的结构,只要最合适的结构。

结局惯例,我如有解说不到位或错漏的地方,期望同学们能够指出。假如有更好的运用办法或封装办法,或许你有遇到的坑也都能够在评论区沟通一下,互相学习进步。

假如感觉本文对你有一点点的协助,还望你能点赞支撑一下,你的支撑是我最大的动力。

本文的部分代码能够在我的 Kotlin 测试项目中看到,【传送门】。你也能够重视我的这个Kotlin项目,我有时刻都会继续更新。

关于 MVVM 架构 和 DataBinding 结构与其他 Jetpack 的实战项目,假如大家有爱好能够看看大佬的项目 难得一见 Jetpack MVVM 最佳实践 。

Ok,这一期就此结束。

findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?

本文正在参与「金石方案」