Android DataBinding基础入门

1,641 阅读10分钟

1.前言

在一个公司呆久了就容易陷入安逸期,技术方面也是如此,很容易止步不前,想要推翻项目原有架构技术重构更是困难重重,但是作为一名程序员,只有不断学习才不会被淘汰,或者说才能跟上行业发展的节奏。

现在对于Android开发来讲,随着行业发展越来越成熟,对人才的要求也越来越高,面试的问题也越来越深,更多是偏向于新技术或者老技术原理性的东西,所以今年准备好好学习下这些,一则为了提高自己的技术能力,这个是非常必要的,二来可以跟上步伐,以应对未来的一切突发状况,因为只有自己变得强大,才有话语权和选择权,所以各位加油吧!

databinding这个词相信很多人并不陌生,很早就见过它,也看过一些博客教程等等,但是并没有实际去使用,后来也就慢慢搁置,其实看名字就可以知道它是个什么东西,就是数据绑定嘛,那么为什么会出现这么个东西,原始的MVC它不香么?是的,它确实不香,各种耦合使得牵一发而动全身,一个activity上千上万行,业务逻辑在其中到处游走,以至于项目越来越臃肿,那么咋办?不要问,问就是重构。

重构是不可能重构的,没事喝杯茶它不香么?正在喝茶期间,突然发现MVP模式已经火了起来,那还等什么,人家有的我都要有,于是MVP开始大放异彩,各种MVP教程满天飞,对于MVP的讨论也越来越多,优缺点也很明显,优点呢,就是项目的耦合度有所降低,它将数据(Model)、视图(View)、控制(Presenter)分开,实现了松耦合。

  • View接受事件,传递给Presenter
  • Presenter做逻辑处理,修改Model
  • Model通知Presenter数据变化,Presenter更新View

看起来非常完美,但是世界上没有什么东西是完美的,它存在如下缺点:

  • MVC一样,P层起到的控制功能伴随着业务的增多,也会变得臃肿。
  • Presneter需要持有View的引用,同时View也需要持有Presenter的引用,控制上存在一定复杂度。

俗话说的好,知足常乐,有点缺点就算了吧,不要斤斤计较,但是对于程序员来讲,都有强迫症,虽然凑合着用吧,但是肯定心里有点不舒服,所以MVVM模式来了,不学习看来真不行,特别是一打开技术博客各种MVVM的文章满天飞的时候。

太难了,终于扯到了今天的主题,MVVM实现了数据与UI的双重绑定,其中DataBinding是实现MVVM的工具,借用网上的一张图来看下MVVM是个什么东西:

M代表了Model,还是负责处理数据层的东西,V代表了View,也就是页面,VM代表了ViewModel,这里有个新引入的东西ViewModel,它是databinding数据绑定的关键所在,下面先介绍一下它。

2.ViewModel

ViewModel官方释义:感知生命周期的形式来存储和管理视图相关的数据。

ViewModel的生命周期很长,存在于所属对象(ActivityFragment)的全部生命周期,可以与jetpack其他组件很方面的结合使用,另外比如activity横竖屏切换,它依然可以保持数据不丢失,也可以用于同一activityfragment的数据共享。

3.databinding具体使用

下面我们就结合databinding和ViewModel来看看基本使用方式,首先引入相关库:

// viewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'

设置databinding支持:

//老版使用这个
/*dataBinding{
        //noinspection DataBindingWithoutKapt
        enabled = true
  }*/

//新版用这个
buildFeatures{
    dataBinding = true
}

如果使用kotlin,还需要添加kotlin-kapt插件:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

此时切换到layout布局,鼠标放在ConstraintLayout,Alt+Enter键会出现如下提示:

继续按回车键,布局就转换为了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:tools="http://schemas.android.com/tools">

    <data>
		//这边用来引入viewmodel类,用于和布局绑定
    </data>

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

        //·····

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

此时我们可以新建一个类继承ViewModel,并设置一个userName变量,用于测试数据绑定:

class MyMainModel : ViewModel() {
    var userName = "张三"
}

然后修改layout布局,引入MyMainModel类,并且把userName设置到Textview上:

<?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>
		//导入MyMainModel类
        <import type="com.xxx.mvvmblog.MyMainModel"/>

		//这边相当于引入数据类,用于绑定布局
        <variable
            name="viewModel"
            type="MyMainModel" />

    </data>

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

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="@color/black"
            android:text="@{viewModel.userName}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/btnChange"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="@color/black"
            android:layout_marginTop="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_name"
            android:text="change"
            />

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

我在TextView下面添加了个Button按钮,点击按钮修改MyMainModeluserName的值,由于数据和布局是绑定的,此时应该布局显示应该会更新,在activity中获取viewmodel,采用如下方式:

ViewModelProvider(this).get(MyMainModel::class.java)

获取databinding实例如下:

DataBindingUtil.setContentView(this, R.layout.activity_main)

那么此时activity代码如下:

class MainActivity : AppCompatActivity() {
    private lateinit var myMainModel: MyMainModel
    private lateinit var activityMainBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        myMainModel = ViewModelProvider(this).get(MyMainModel::class.java)
        activityMainBinding.viewModel = myMainModel

        initView()
    }

    private fun initView() {

        findViewById<Button>(R.id.btnChange).setOnClickListener {

            myMainModel.userName = "我是改变后的值:李四"
            activityMainBinding.viewModel = myMainModel
        }
    }
}

来,运行下,看看效果:

有那么回事了,通过databinding修改布局中绑定的viewmodel类的值,就可以自动更新ui,这也就是我们看到的单向绑定,何为单向,意思就是数据改变,布局改变,那既然有单向,肯定有双向,是的,双向则是指相互可以更新,我们用EditText来演示下,修改下布局:

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="10dp"
    android:hint="请输入"
    android:text="@={viewModel.userName}"
    app:layout_constraintBottom_toTopOf="@+id/tv_name"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

单向绑定采用@{}的形式设置数据,双向使用@={},中间加了个=号,activity代码也相应修改下:

插播一下,写着写着发现findViewById太烦了,索性添加了如下插件:

id 'kotlin-android-extensions'

此时就可以直接使用布局中的id作为组件对象,太嗨皮了!!

咱们接着修改activity代码:

editText.addTextChangedListener {
    Log.e("userName","new userName = ${activityMainBinding.viewModel!!.userName}")
}

来看看效果:

因为我们把userNameEdittext进行了双向绑定,所以我们在修改EditText的时候,相应的数据源也会发生改变,我们把viewModel给打印了出来,可以发现此时数据是一直在变的,我们再修改下代码:

editText.addTextChangedListener {
    Log.e("userName","new userName = ${activityMainBinding.viewModel!!.userName}")
	//加上这句,Edittext变化后通知更新Textview
    activityMainBinding.viewModel = myMainModel
}

效果如下:

注意:

1.类名字重复,使用别名

如果导入的类名字重复,可以使用别名alias指定别名
<data>
    <import type="com.xxx.mvvmblog.MyMainModel" 
        alias="AliasName"
        />

    <variable
        name="viewModel"
        type="AliasName" />
</data>

2.多variable

可以指定多个variable,如果使用<>,需要使用转义字符:
<data>
    <import type="java.util.List" />
    <import type="com.xxx.mvvmblog.MyMainModel" />

    <variable
        name="list"
        type="List&lt;MyMainModel&gt;" />

    <variable
        name="viewModel"
        type="MyMainModel" />
</data>

3.修改databinding名字

默认的Binding类生成的规则是:xml的名称去掉下划线,再首字母大写,最后尾部加个Binding,当然这个事可以自定义的,使用class关键字来修改,形如:

<data class="BindingName">
</data>

4.绑定方法

三种方式:

  • 普通方法调用:android:onClick="@{()->viewModel.clickMe()}"
fun clickMe(){
    Log.e("click","click me -- 方法")
}
  • 双冒号调用:android:onClick="@{viewModel::clickMe}"
fun click(view: View){
    Log.e("click","click me: $view, 使用::调用,方法定义需要与接口参数一致")
}
  • 变量调用:android:onClick="@{viewModel.clickMe}"
val clickMe = View.OnClickListener {
    Log.e("click", "click me -- 变量")
}

5.转换器

如果我们想要对某类数据进行单独处理,比如想在所有字符串后面添加(试用版)标识,那么就可以使用转化器BindingConversion,具体使用方式如下,定义:

object ConvertionTest {

    @JvmStatic
    @BindingConversion
    fun convert(value: String?) = "${value}(试用版)"
}

效果:

我们设置username后,在绑定更新ui的时候结尾会自动加上(试用版)几个字。

6.自定义属性绑定方法

一、在view中,如果提供setXxxx方法,就有xxxx属性,我们可以直接在layout中使用该属性,比如selected属性,Textview在布局中并没有提供此属性,但是代码中有setSelected方法,所以我们可以这么用:

app:selected="@{true}"

鼠标放上去按住ctrl键,可以看到相关属性和方法:

二、自定义属性绑定方法

比如我们想要自定义一个TextView,一旦TextView文本有变化,就弹出toast,那么我们可以这样做,使用@BindingMethods注解,定义自定义属性:

@BindingMethods(
    value = [
        BindingMethod(
            type = TextView::class,
            attribute = "showToast",
            method = "show"
        )]
)
class MyTextView(context: Context?, attrs: AttributeSet?) :
    AppCompatTextView(context!!, attrs) {

    fun show(toastStr: String) {
        Toast.makeText(context, "哈哈哈,$toastStr", Toast.LENGTH_LONG).show()
    }
}

attribute对应的就是xml中的属性,method则对应我们自定义的show方法,我们在xml中使用它:

<com.xxx.mvvmblog.MyTextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@{viewModel.userName}"
    android:textColor="@color/black"
    android:textSize="20sp"
    app:showToast="@{viewModel.userName}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

看下效果:

通过 BindingMethod 注解,可以自定义属性名,并绑定指定方法。当绑定的数据变化时,会自动调用该方法。

7.自定义多属性绑定

@BindingAdapter这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用,我们都知道imageview并不支持直接加载网络图片,那么我们在使用databinding的时候,可以通过自定义属性的方式实现此功能,只需指定图片地址即可实现自动加载图片,那么实现起来也很简单,通过BindingAdapter自定义一个imageUrl属性:

class ImageLoader {
    companion object {
        @JvmStatic
        @BindingAdapter("imageUrl", "placeholder", "error",requireAll = false)
        fun loadImageView(
            imageView: ImageView,
            url: String?,
            drawHolder: Drawable?,
            error: Drawable?
        ) {
            Glide.with(imageView).load(url).into(imageView)
        }
    }
}

可以看到我们定义了imageUrl,placeholder,error等属性,当然你可以随便添加其他需要的属性,这边还设置了requireAll为false,它的含义是设置可以不配置所有属性,意思就是如果requireAll为true,需要指定全部自定义的属性,那么我们看下如何使用此属性:

<ImageView
    android:layout_width="200px"
    android:layout_height="200px"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/editText"
    imageUrl="@{`https://hbimg.huabanimg.com/52c4c08a08e719e1e2cad797fe53feb97917b6fa441d-I5Yl20_fw658/format/webp`}"
    />

也很简单,指定需要的参数和属性即可,然后运行看下效果:

转换器-自定义属性-图片加载.gif

可以看到,图片已经加载并显示出来了,此处仅仅抛砖引玉,你可以充分发挥自己的灵感,做其他任何类似的事情。

4.结合LiveData使用

首先,LiveData是什么呢?LiveData是一个可以被观察的数据持有类,它可以感知 ActivityFragmentService等组件的生命周期,它有如下优点:

  • 在组件处于激活状态的时候才会回调相应的方法,从而刷新相应的ui;

  • 避免了内存泄漏;

  • config导致activity重新创建的时候,不需要手动去处理数据的储存和恢复,它已经帮我们封装好了;

先看个简单的例子吧,首先引入LiveData库:

 // liveData
 api 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0'

然后在mainviewModel中加入如下代码:

//新建一个数据类Student,测试用
data class Student(val name: String)

class MyMainModel : ViewModel() {
    val studentLiveData = MutableLiveData<Student>()
    ······
}

然后我们在MainActivity中获取viewmodel实例,并且为studentLiveData添加监听,此时一旦有数据变化,并且activity处于活跃状态下,就会回调observe,来看关键代码:

myMainModel = ViewModelProvider(this).get(MyMainModel::class.java)
activityMainBinding.viewModel = myMainModel

myMainModel.studentLiveData.observe(this, Observer {
    //数据有更新/变化
    myMainModel.userName = it.name
    activityMainBinding.viewModel = myMainModel
})

然后我们模拟一下数据请求:

private fun loadData() {
    launch {
        withContext(Dispatchers.IO) {
            delay(2000)
        }

        val student = Student("我是新来的学生")
        myMainModel.studentLiveData.postValue(student)
    }
}

当我们点击按钮的时候,开始执行loadData方法:

findViewById<Button>(R.id.btnChange).setOnClickListener {
    loadData()
}

然后我们看下效果:

转换器-自定义属性-livedata.gif

可以发现,我们点击按钮2秒后,通过postValue方法更新了studentLiveData的值,此时会回调observe方法,然后进行ui的刷新等操作,这个过程就和我们平时进行网络请求的过程非常类似。

livedata可以感知生命周期的含义就是,比如你在A页面进行observe监听,然后跳转到页面B,此时通过postValue更新数据(当然我们也可以使用setValue,区别就是setValue只能在主线程发送数据,post可以在子线程发送数据),此时是不回回调observe的,当你点击返回键,回到A页面的时候,才会触发observe回调,这样就避免了内存泄漏的风险,这个过程大家可以自行尝试并感知。

注意:如果在布局中直接绑定的livedata数据源,形如:

<com.xxx.mvvmblog.MyTextView
    android:id="@+id/tv_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    //注意这一行
    android:myText="@{viewModel.studentLiveData.name}"
    android:textColor="@color/black"
    ······/>

上面的例子我是直接使用的mytext="@{viewModel.userName}"的方式更新的数据,此时会刷新ui,当时如果用了viewModel.studentLiveData.name,就需要databinding设置LifecycleOwner才会触发ui的更新,也就是要给databinding加上如下代码:

activityMainBinding.lifecycleOwner = this

这样通过改变liveData的数据就可以触发绑定其数据源的ui组件进行更新,换句话说就是DataBinding需调用setLifecycleOwner(LifecycleOwner lifecycleOwner)之后,绑定了LiveData数据源的xml控件才会随着数据变化而改变。

本篇主要讲解了基础使用,文中必然存在一些问题和错误,轻喷,欢迎指正.

参考: