当你真的学会DataBinding后,你会发现“这玩意真香”!-灵析社区

江江说技术

前言

🏀DataBinding只是一种工具,用来解决View和数据之间的绑定。

Data Binding,顾名思义:数据绑定,它可以将布局页面中的组件和应用中的数据进行绑定,支持单向绑定和双向绑定,单向绑定就是如果数据有变化就会驱动页面进行变化,双向绑定就是除了单向绑定之外还支持页面的变化驱动数据的变化,如果页面中有一个输入框,那么我们就可以进行双向绑定,数据变化,它的显示内容就变了,我们手动输入内容也可以改变绑定它的数据。

🌟官方文档:developer.android.google.cn/jetpack/and…

🌟官方Demo地址:github.com/googlecodel…

如何使用DataBinding呢?

1.启用DataBinding

引用官方文档: Databinding与 Android Gradle 插件捆绑在一起。您无需声明对此库的依赖项,但必须启用它。 ☀注意:即使模块不直接使用数据绑定,也必须为依赖于使用数据绑定的库的所有模块启用数据绑定。
//在gradle的android下加入,然后点击sync
android {
    ...
    //android studio 4.0以下
    dataBinding{
    
    }
    //android studio4.1以后
    buildFeatures {
        dataBinding true
    }
}

2.生成DataBinding布局

在我们的布局文件中,选择根目录的View,按下Alt+回车键,点击Convert to data binding layout,就可以转换为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>
​
    </data>
​
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
​
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
​
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

我们可以发现,最外面变成了layout元素,里面有data元素。我们将在data元素中声明这个布局中使用到的变量,以及变量的类型。

举个例子:

<data>
    <import type="com.example...."/>
    <variable
        name="color"
        type="java.lang.String" />
</data>
  • data: 在标签内进行变量声明和导入等
  • variable: 进行变量声明
  • import: 导入需要的类

3.声明一个User实体类

class User() {
    var name = "Taxze"
    var age = 18
    fun testOnclick() {
        Log.d("onclick", "test")
    }
}

4.在xml中使用

然后在data中声明变量,以及类名

<data>
    <!-- <variable-->
    <!-- name="user"-->
    <!-- type="com.taxze.jetpack.databinding.User" />-->
    <import type="com.taxze.jetpack.databinding.User" />
​
    <variable
        name="user"
        type="User" />
</data>

然后在布局中使用@{}语法

//伪代码,请勿直接CV
<TextView
    ...
    android:text="@{user.name}"
/>

5.在Activity或Fragment中使用DataBinding

在Activity中通过DataBindingUtil设置布局文件,同时省略Activity的setContentView方法

class MainActivity : AppCompatActivity() {
    private lateinit var mainBinding: ActivityMainBinding
    private lateinit var mainUser: User
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        mainUser = User()
        mainBinding.user = mainUser
    }
}

在Fragment中使用:

class BlankFragment : Fragment() {
    private lateinit var mainFragmentBinding:FragmentBlankBinding
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        mainFragmentBinding = DataBindingUtil.inflate(inflater,R.layout.fragment_blank,container,false)
        return mainFragmentBinding.root
    }
}

系统会为每个布局文件都生成一个绑定类。一般默认情况下,类的名称是布局文件名称转化为Pascal大小写形式,然后在末尾添加Binding后缀,例如:名称为activity_main的布局文件,对应的类名就是ActivityMainBinding

运行之后的效果:

注意:只有当布局文件转换为layout样式之后,databinding才会根据布局文件的名字自动生成一个对应的binding类,你也可以在build/generated/data_binding_base_source_out目录下查看生成的类


最最最基础的使用就是这样,接下来我们来讲讲如何更好的使用DataBinding

如何在xml布局中更好的使用DataBinding

1.使用集合中的元素

  • 加入我们传入了一个集合books,我们可以通过以下方式使用: 获取集合的值
android:text="@{books.get(0).name}" android:text="@{books.[0].name}" 

添加默认值(⚡默认值无需加引号,且只在预览视图显示)

 android:text="@{books.pages,default=330}" 

通过??或?:来实现

 android:text="@{books.pages != null ? book.pages : book.defaultPages}" xml复制代码android:text="@{books.pages ?? book.defaultPages}"

2.使用map中的数据

  • map类型的结构也可以通过get和[]两种方式获取
//需要注意单双引号 android:text="@{books.get('name')}" android:text="@{books['name']}"

3.转换数据类型

  • 因为DataBinding不会自动做类型转换,所有我们需要手动转换,例如在text标签内使用String.valueOf()转换为String类型,在rating标签内我们可以使用Float.valueOf()进行转换
android:text="@{String.valueOf(book.pages)}" 


android:rating="@{Float.valueOf(books.rating),default=2.0}"

4.导入包名冲突处理

  • 如果我们导入的包名有冲突,我们可以通过alias为它设置一个别名
//伪代码,请勿直接CV <data> <import type="com.xxx.a.Book" alias="aBook"/> <import type="com.xxx.B.Book" alias="bBook"/> ... </data>

5.隐式引用属性

  • 在一个view上引用其他view的属性
//伪代码,请勿直接CV <import type="android.view.View"/> ... <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> <CheckBox android:id="@+id/checkOne" .../> <ImageView android:visibility="@{checkOne.checked ? View.VISIBLE : View.GONE}" .../> </LinearLayout>
  • **include标签和ViewStub**标签 include和merge标签的作用是实现布局文件的重用。就是说,为了高效复用及整合布局,使布局轻便化,我们可以使用include和merge标签将一个布局嵌入到另一个布局中,或者说将多个布局中的相同元素抽取出来,独立管理,再复用到各个布局中,便于统一的调整。 比如,一个应用中的多个页面都要用到统一样式的标题栏或底部导航栏,这时就可以将标题栏或底部导航栏的布局抽取出来,再以include标签形式嵌入到需要的布局中,而不是多次copy代码,这样在修改时只需修改一处即可。而我们同样可以通过DataBinding来进行数据绑定。
  • 例如:
//layout_title.xml <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="com.xxx.User" /> <variable name="userInfo" type="User" /> </data> <android.support.constraint.ConstraintLayout ... > <TextView ... android:text="@{userInfo.name}" /> </android.support.constraint.ConstraintLayout> </layout>

使用该布局,并传值

<include layout="@layout/layout_title" bind:test="@{userInfo}"/>
  • ViewStub也是类似的用法,这里就不说了。
  • DataBinding不支持merge标签

6.绑定点击事件


//伪代码,请勿直接CV onclick="@{()->user.testOnclick}" onclick="@{(v)->user.testOnclick(v)}" onclick="@{()->user.testOnclick(context)}" onclick="@{BindHelp::staticClick}" onclick="@{callback}" ​ //例如: <Button android:layout_width="match_parent" android:layout_height="match_parent" android:onClick="@{()->user.testOnclick}" />
💡文章到这里讲的都是DataBinding如何设置数据,以及通过DataBinding在xml中的一些基础使用。如果只是使用DataBinding这个功能,那就有点大材小用了。它还有一个很强大的功能我们还没有讲,那就是数据更新时自动刷新UI。

实现数据变化时自动更新UI

一个普通的实体类或者ViewModel被更新后,并不会让UI自动更新。而我们希望,当数据变更后UI要自动更新,那么要实现数据变化时自动更新UI,有三种方法可以使用,分别是BaseObservableObservableFieldObservableCollection

💡单向数据绑定:

  • BaseObservable
BaseObservable提供了两个刷新UI的方法,分别是 notifyPropertyChanged() 和 notifyChange() 。 

第一步:修改实体类

将我们的实体类继承与BaseObservable。需要响应变化的字段,就在对应变量的get函数上加 @Bindable 。然后set中notifyChange是kotlin的写法,免去了java的getter setter的方式。成员属性需要响应变化的,就在其set函数中,notify一下属性变化,那么set的时候,databinding就会感知到。 
import androidx.databinding.BaseObservable import androidx.databinding.Bindable import androidx.databinding.library.baseAdapters.BR ​ class User() : BaseObservable() { constructor(name: String, age: Int) : this() { this.name = name this.age = age } //这是单独在set上@bindable,name可以为声明private var name: String = "" set(value) { field = value notifyPropertyChanged(BR.name) } @Bindable get() = field ​ //这是在整个变量上声明@bindable,所以必须是public的 @Bindable var age:Int = 18 set(value) { field = value notifyPropertyChanged(BR.age) } get() = field }

第二步:在Activity中使用

kotlin复制代码class MainActivity : AppCompatActivity() { private val TAG = "MainActivity" private lateinit var mainBinding: ActivityMainBinding private lateinit var mainUser: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mainBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main) mainUser = User("Taxze", 18) mainUser.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { when { BR.user == propertyId -> { Log.d(TAG, "BR.user") } BR.age == propertyId -> { Log.d(TAG, "BR.age") } } } }) mainBinding.user = mainUser mainBinding.onClickPresenter = OnClickPresenter() } ​ inner class OnClickPresenter { fun changeName() { mainUser.name = "Taxze2222222" } } } 
  • 需要注意的点

官方网站只是提示了开启DataBinding只需要在build.gradle中加入下面这行代码

buildFeatures { dataBinding true } 

但是,如果你想更好的使用DataBinding这是不够的,你还需要添加这些配置:

compileOptions { 
          sourceCompatibility JavaVersion.VERSION_1_8
          targetCompatibility JavaVersion.VERSION_1_8 
}
 kotlinOptions {
           jvmTarget = '1.8' 
} 

🔥重点:在使用DataBinding得时候,BR对象,发现调用不了,生成也会报错,运行,需要在咱们build.gradle中进行一下配置:

apply plugin: 'kotlin-kapt' kapt { generateStubs = true } 

然后重新运行一遍代码,你就会发现,BR文件自动生成啦!

  • ObservableField
 讲解了BaseObservable后,现在来将建最简单也是最常用的。只需要将实体类变化成这样即可: 
//注意observable的属性需要public权限,否则dataBinding则无法通过反射处理数据响应 class User() : BaseObservable() { var name: ObservableField<String> = ObservableField("Taxze") var age:ObservableInt = ObservableInt(18) }
  • ObservableCollection
 dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap

实体类修改:

//伪代码,请勿直接cv class User(){ var userMap = ObservableArrayMap<String,String>() } //使用时: mainUser.userMap["name"] = "Taxze" mainUser.userMap["age"] = "18" 

使用ObservableCollection后,xml与上面的略有不同,主要是数据的获取,需要指定Key值

//伪代码,请勿直接cv ... <import type="android.databinding.ObservableMap" /> <variable name="userMap" type="ObservableMap<String, String>" /> ​ //使用时: <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="Name" android:text="@{userMap[`userName`]}" />

💡双向数据绑定:

  • 只需要在之前的单向绑定的基础上,将布局文件@{}变为@={},用于针对属性的数据改变的同时监听用户的更新

DataBinding在RecyclerView中的使用

在RecyclerView中使用DataBinding稍有变化,我们在ViewHolder中进行binding对象的产生,以及数据对象的绑定。

我们通过一个非常简单的例子来讲解如何在RecyclerView中使用DataBinding。

效果图:

  • 第一步:创建实体类 就是我们之前的,使用了BaseObservable的那个实体类,这里就不放代码了
  • 第二步:创建activity_main用于存放recyclerview
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activty_main" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> ​ <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="wrap_content" /> ​ </RelativeLayout>
  • 第三步:创建text_item.xml用于展示recyclerview中的每一行数据
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android"> ​ <data> <import type="com.taxze.jetpack.databinding.User" /> <variable name="user" type="User" /> </data> ​ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> ​ <LinearLayout android:layout_width="match_parent" android:layout_height="40dp" android:background="#ffffff" android:orientation="horizontal" android:paddingStart="10dp"> ​ <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:text="@{`这个人的姓名是` + user.name}" /> ​ <TextView android:id="@+id/tv_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:layout_marginLeft="20dp" android:text="@{String.valueOf(user.age)}" /> </LinearLayout> </LinearLayout> </layout>
  • 第四步:创建Adapter 有了之前的基础之后,大家看下面这些代码应该很容易了,就不做过多讲解啦
import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.recyclerview.widget.RecyclerView import com.taxze.jetpack.databinding.databinding.TextItemBinding ​ class FirstAdapter(users: MutableList<User>, context: Context) : RecyclerView.Adapter<FirstAdapter.MyHolder>() { //在构造函数中声明binding变量,这样holder才能引用到,如果不加val/var,就引用不到,就需要在class的{}内写get函数 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) { //java 写法可以setVariable holder.binding.user = users[position] holder.binding.executePendingBindings() } //kotlin中,return的方式,可以简写 override fun getItemCount() = users.size }
  • 第五步:在MainActivity中使用
import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView ​ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initView() } //给RecyclerView设置数据 private fun initView() { val recyclerView = findViewById<View>(R.id.recyclerView) as RecyclerView recyclerView.layoutManager = LinearLayoutManager(this) val users: MutableList<User> = ArrayList() for (i in 0..100) { val user = User() user.name = "Taxze" user.age = i users.add(user) } val adapter = FirstAdapter(users, this) recyclerView.adapter = adapter } }

这样就完成了在RecyclerView中使用DataBinding啦。

高级用法

第一个:用于appCompatImageView的自定义属性

//伪代码,请勿直接cv
/**
 * 用于appCompatImageView的自定义属性,bind:imgSrc,命名空间bind:可以省略,也就是写作 imgSrc亦可。可以用于加载url的图片
 * 函数名也是随意,主要是value的声明,就是新加的属性名了,可以多个属性同用,并配置是否必须一起作用
 * 函数名随意,方法签名才重要,匹配对象控件,以及属性参数。
 * 这里还可以添加old 参数,获取修改新参数 之前对应的值。
 * todo 加载网络图片,需要网络权限!!!
 */
@JvmStatic
@BindingAdapter(value = ["bind:imgSrc"], requireAll = false)
fun urlImageSrc(view: AppCompatImageView, /*old: String?, */url: String) {
    Glide.with(view)
        .load(url)
        .placeholder(R.drawable.img_banner)
        .centerInside()
        .into(view)
}

第二个:配合swipeRefreshLayout的刷新状态的感知

  • 第一步:单向的,数据变化,刷新UI
//伪代码,请勿直接cv @JvmStatic @BindingAdapter("sfl_refreshing", requireAll = false) fun setSwipeRefreshing(view: SwipeRefreshLayout, oldValue: Boolean, newValue: Boolean) { //判断是否是新的值,避免陷入死循环 if (oldValue != newValue) view.isRefreshing = newValue }
  • 第二步:ui的状态,反向绑定给数据变化
//伪代码,请勿直接cv @JvmStatic @BindingAdapter("sfl_refreshingAttrChanged", requireAll = false) fun setRefreshCallback(view: SwipeRefreshLayout, listener: InverseBindingListener?) { listener ?: return view.setOnRefreshListener { //由ui层的刷新状态变化,反向通知数据层的变化 listener.onChange() } }
  • 第三步: 反向绑定的实现
 //伪代码,请勿直接cv /** * 反向绑定的实现,将UI的变化,回调给bindingListener,listener就会onChange,通知数据变化 * 注意这里的attribute和event,是跟上面两步配合一致才有效 */ @JvmStatic @InverseBindingAdapter(attribute = "sfl_refreshing", event = "sfl_refreshingAttrChanged") fun isSwipeRefreshing(view: SwipeRefreshLayout): Boolean { return view.isRefreshing }

DataBinding配合ViewModel&LiveData一起使用

我将通过一个简单的例子带大家学习他们如何一起使用,话不多说,先上效果图:

  • 第一步:创建UserModel
//将其继承于AndroidViewModel(AndroidViewModel也是继承于ViewModel的,但是ViewModel本身没有办法获得 Context,AndroidViewModel提供Application用作Context,并专门提供 Application 单例) ​ //UserName 使用MutableLiveData class UserModel(application: Application) : AndroidViewModel(application) { var UserName = MutableLiveData("") }
  • 第二步:创建activity_main和对应的MainActivity
<?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> <variable name="loginModel" type="com.taxze.jetpack.databinding.model.UserModel" /> </data> ​ <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> ​ <LinearLayout android:id="@+id/linearLayout" style="@style/InputBoxStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="17dp" android:layout_marginEnd="17dp" app:layout_constraintBottom_toTopOf="@+id/guideline2" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" tools:ignore="MissingConstraints"> ​ <EditText android:id="@+id/editText" style="@style/EditTextStyle" android:layout_width="match_parent" android:layout_height="50dp" android:hint="请输入账号" android:text="@={loginModel.UserName}" tools:ignore="MissingConstraints" /> </LinearLayout> ​ <TextView android:id="@+id/textView2" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:text='@{"您输入的账号名是:"+loginModel.UserName,default=123123123}' android:textSize="24sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button" tools:ignore="MissingConstraints" /> ​ <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.4" /> ​ <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="32dp" android:background="@drawable/button_drawable" android:text="登录" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/linearLayout" tools:ignore="MissingConstraints" /> </androidx.constraintlayout.widget.ConstraintLayout> ​ </layout>
  • 第三步:在MainActivity中绑定页面和绑定声明周期
class MainActivity : AppCompatActivity() { lateinit var viewDataBinding: ActivityMainBinding lateinit var model: UserModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //绑定页面 viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main) //绑定生命周期 viewDataBinding.lifecycleOwner = this model = ViewModelProvider.AndroidViewModelFactory.getInstance(this.application) .create(UserModel::class.java) viewDataBinding.loginModel = model ​ ... } }
  • 第四步:传值
viewDataBinding.button.setOnClickListener { val intent = Intent(MainActivity@ this, SecondActivity::class.java) intent.putExtra("user", "${model.UserName.value}") startActivity( intent ) }
  • 第五步:在另外一个activity中调用
class SecondActivity : AppCompatActivity() { lateinit var viewDataBinding: ActivitySecondBinding lateinit var userName: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //绑定页面 viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_second) //绑定生命周期 viewDataBinding.lifecycleOwner = this ​ userName = intent.getStringExtra("user").toString() ​ viewDataBinding.tvName.text = "登录的账号是:$userName" } }

帮你踩坑🍖:

  • TextViewtext属性,需要注意data不能为Number类型
  • 反射属性、函数必须是public
  • observableField数据的时候,在某些场合需要初始化,否则会运行报错!
  • 使用LiveData作为dataBinding的时候,需要在ui中设置binding.lifecycleOwner
  • 名为ALuoBo的读者补充:xml中可以使用中文字符,格式如下: android:text='@{state.isChinese? "确定" :"OK"}'

尾述

这篇文章已经很详细的讲了DataBinding的大部分用法,不过在看完文章后,你仍需多多实践,相信你很快就可以掌握DataBinding啦😺 有问题欢迎在评论区留言讨论~

阅读量:2007

点赞量:0

收藏量:0