在传统的Android应用开发中,布局文件通常只负责应用界面的布局工作,如果需要实现页面交互就需要调用setContentView()将Activity、fragment和XML布局文件关联起来。然后通过控件的id找到控件,接着在页面中通过代码对控件进行逻辑处理。在这种传统的开发方式中,页面承担了大部分的工作量,大量的逻辑处理需要在Activity、Fragment中进行处理,因此页面显得臃肿不堪,维护起来也很困难,为了减轻页面的工作量,Google提出了DataBinding(视图绑定)。
一、DataBinding的优点?
(1)解耦。将部分原属于Activity/Fragment去实现的功能交由Model实体类去实现,从而实现了部分功能代码的分离,既保证了Activity/Fragment过于臃肿,也便于后期代码的维护和扩展。
(2)避免重复无效代码。不再需要findViewById操作(其实这条不是那么重要,因为采用kotlin编码的话本身就没有findViewById)。
总结:Databinding的作用就是在XML文件里面实现数据的部分绑定从而避免将数据的全部展示交由Controller层去实现从而达到代码的易于扩展和后期的维护目的。
二、DataBinding的基本使用
先来了解一下DataBinding常用的几个类:
- DataBindingUtil:在Activity/Fragment中获取相关的Binding对象。
- BaseObservable:Bean可以继承该抽象类,实现可观察的模式,在set属性的时候调用notifyPropertyChanged方法,唤起刷新操作,也可以调用notifyChange方法全部刷新。
- Observable:Bean可以实现该接口,实现可观察的模式,在set属性的时候调用notifyPropertyChanged方法,唤起刷新操作,也可以调用notifyChange方法全部刷新。
- ObservableFloat:这不是一个类,而是一类类的代表,如ObservableShort、ObservableParcelable等等,可观察的属性,通过get和set方法操作相关的值。
- BaseObservableField<>:和上述类似,泛型可以传入String等类型,比上述定义的基类型更加自由。
android {
...
dataBinding {
enabled = true
}
}
2.修改布局文件
以<layout>
为头,以</layout>
为尾。
<layout>
<data>
<variable
name="login"
type="com.jack.androidjetpack.login.Login" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
3.点击Build->Rebuild Project生成对应的Binding
4.使用DataBindingUtil类来进行视图的绑定
Activity的处理方式
class LoginActivity : AppCompatActivity() {
var binding: ActivityLoginBinding? = null
var login: Login? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
login = Login("jack", "123456")
binding?.setLogin(login)
}
}
Fragment的处理方式
class PhoneCodeFragment : Fragment() {
private var param1: String? = null
private var param2: String? = null
private var binding: FragmentPhoneCodeBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentPhoneCodeBinding.inflate(inflater, container, false)
return binding?.root
}
companion object {
@JvmStatic
fun newInstance(param1: String, param2: String) =
PhoneCodeFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
5.在layout布局中添加data标签
经过前面的几个步骤,我们已经将Databinding和我们的XML文件绑定起来了,现在你点击Databinding会发现直接可以跳转到对应的XML文件里面去了,现在我们就来看看如何给我们的XML文件里面的View设置值。
在XML文件的layout标签下,创建data标签,在data标签中再创建variable标签,variable标签主要用到的就是name属性和type属性,类似于Java语言声明变量时,需要为该变量指定类型和名称。新建一个名为Login的数据类。
data class Login(var userName: String, var password: String):BaseObservable()
然后在布局的 data 标签里声明要使用到的变量名、类的全路径等信息,如下所示:
<layout>
<data>
<variable
name="login"
type="com.jack.androidjetpack.login.Login" />
</data>
...
</layout>
如果 Login有多处用到,也可以直接将之 import 进来,这样就不用每次都指明整个包名路径了,而 java.lang.* 包中的类会被自动导入,所以可以直接使用。
<layout>
<data>
<import type="com.jack.androidjetpack.login.Login" />
<variable
name="login"
type="Login" />
</data>
...
</layout>
在XML文件中声明好variable属性后,接下来就可以在XML使用它了。使用variable属性时需要使用到布局表达式: @{ }
。可以在布局表达式@{ }
中获取传入variable对象的值,如下所示:
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<EditText
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="96dp"
android:layout_marginEnd="24dp"
android:hint="@string/prompt_name"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
android:text="@{login.userName}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
android:text="@{login.password}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
</androidx.constraintlayout.widget.ConstraintLayout>
最后在我们的Controller
层将我们的data
与model
相关联。
var binding: ActivityLoginBinding? = null
var login: Login? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
login = Login("jack", "123456")
binding?.setLogin(login)
}
到这里,一个最基础的DataBinding的例子就结束了,接下来我们继续往下看。
三、DataBinding的进阶
进阶1:给控件添加响应事件
方式一:直接在Controller层通过原来的方式添加
binding?.login?.setOnClickListener {
}
方式二:创建一个工具类,在类中定义响应的点击事件
第一步:创建点击的工具类
/**
* @author: zhoufan
* @date: 2021/8/30 11:14
*/
class ButtonClickListener {
fun click(view: View) {
Log.e("click","响应登录的点击事件")
}
}
第二步:在XML文件中添加工具类
<variable
name="btnHandler"
type="com.jack.androidjetpack.login.ButtonClickListener" />
第三步:在XML文件中添加响应事件
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{btnHandler::click}"
android:text="@string/action_sign_in"
/>
第四步:在Controller里面进行关联
class LoginActivity : AppCompatActivity() {
var binding: ActivityLoginBinding? = null
var login: Login? = null
var clickListener:ButtonClickListener?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
login = Login("jack", "123456")
clickListener= ButtonClickListener()
binding?.setLogin(login)
// 这一步必须要,否则点击没反应
binding?.btnHandler = clickListener
}
}
进阶2:BindingAdapter
使用DataBinding库时,DataBinding会针对控件属性生成对应的XXXBindingAdapter类,如TextViewBindingAdapter类,其对TextView的每个可以使用DataBinding的属性都生成了对应的方法,而且每个方法都使用了@BindingAdapter注解,注解中的参数就是对应View的属性。
自定义BindingAdapter
编写一个处理图片的自定义BindingAdapter类。然后定义一个静态方法,主要用于添加 BindingAdapter 注解,注解值是 ImageView 控件自定义的属性名,如下所示。
class ImageBindingAdapter {
companion object {
@BindingAdapter("url")
@JvmStatic
fun loadImage(view: ImageView?, url: String?) {
var realValue: String? = null
if (url.equals("null")) {
realValue = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201412%2F27%2F111335whdlgodddosl6swq.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632887161&t=f6a13d2953ea7be364a111344b72a654"
}
if (!TextUtils.isEmpty(realValue)) {
Glide.with(view!!)
.load(realValue)
.into(view)
}
}
}
}
在XML文件里面直接引用
<ImageView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_below="@+id/login"
android:layout_marginTop="16dp"
app:url="@{`null`}" />
有时候,我们需要自定义多个属性,那如何处理呢?和一个参数一样,我们只需要使用BindingAdapter添加参数即可,如下所示:
class ImageBindingAdapter {
companion object {
@BindingAdapter(value = ["url", "placeholder", "error"])
@JvmStatic
fun loadImage(view: ImageView?, url: String?, placeholder: Drawable?, error: Drawable?) {
var realValue: String? = null
if (url.equals("null")) {
realValue =
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201412%2F27%2F111335whdlgodddosl6swq.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632887161&t=f6a13d2953ea7be364a111344b72a654"
}
if (!TextUtils.isEmpty(realValue)) {
val options = RequestOptions()
options.placeholder(placeholder)
options.error(error)
Glide.with(view!!)
.load(realValue)
.apply(options)
.into(view)
}
}
}
}
对应的XML文件为:
<ImageView
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_below="@+id/login"
android:layout_marginTop="16dp"
app:placeholder="@{ContextCompat.getDrawable(context, R.mipmap.ic_launcher)}"
app:error="@{ContextCompat.getDrawable(context, R.mipmap.ic_launcher)}"
app:url="@{`null`}" />
别忘记导入对应的依赖
<import type="androidx.core.content.ContextCompat" />
<import type="com.jack.androidjetpack.R"/>
进阶3:双向绑定
DataBinding的本身是对View层状态的一种观察者模式的实现,通过让View与ViewModel层可观察的对象进行绑定,当ViewModel层数据发生改变时,View层也会自动进行UI的更新,这种场景称之为单向绑定。
但是在实际的开发过程中,单向绑定并不能满足所有的需求。例如有下面的场景:如果布局中有一个EditText,当用户在输入框中输入内容时,我们希望对应的Model类能够实时更新,这就需要双向绑定,DataBinding同样支持这样的能力。
实现双向绑定需要用到ObservableField
类,它能够将普通的数据对象包装成一个可观察的数据对象,数据类型可以是基本数据类型、变量、集合,也可以是自定义类型。
第一步:修改我们的实体类
class Login : BaseObservable() {
@get:Bindable
var userName: String? = null
set(userName) {
field = userName
notifyPropertyChanged(BR.userName)
}
@get:Bindable
var password: String? = null
set(password) {
field = password
notifyPropertyChanged(BR.userName)
}
}
第二步:修改我们的实体类
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginStart="24dp"
android:layout_marginTop="96dp"
android:layout_marginEnd="24dp"
android:hint="@string/prompt_name"
android:inputType="textEmailAddress"
android:selectAllOnFocus="true"
android:text="@={login.userName}" />
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@+id/username"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:hint="@string/prompt_password"
android:imeActionLabel="@string/action_sign_in_short"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:selectAllOnFocus="true"
android:text="@={login.password}" />
将我们原来的@{login.userName}
换成@={login.userName}
就可以了。
四、DataBinding的实战
在我们的实际开发过程中,RecyclerView算是使用非常频繁的,接下来我们就看看如何使用DataBinding对RecyclerView进行处理。
第一步:定义我们的实体类
class UserModel: BaseObservable() {
@get:Bindable
var name: String? = null
set(name) {
field = name
notifyPropertyChanged(BR.name)
}
@get:Bindable
var address: String? = null
set(address) {
field = address
notifyPropertyChanged(BR.address)
}
@get:Bindable
var age: String? = null
set(age) {
field = age
notifyPropertyChanged(BR.age)
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.jack.androidjetpack.list.UserModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.address}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.age}" />
</LinearLayout>
</layout>?
class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {
var dataList: MutableList<UserModel> = mutableListOf()
set(value) {
field = value
notifyDataSetChanged()
}
inner class ViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
var adapterBinding: AdapterListBinding? = null
init {
adapterBinding = binding as AdapterListBinding
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.adapter_list,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val userModel = dataList[position]
holder.adapterBinding?.user = userModel
}
override fun getItemCount() = dataList.size
}
class ListActivity : AppCompatActivity() {
private var userModelList: MutableList<UserModel>? = mutableListOf()
private var activityListBinding: ActivityListBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityListBinding = DataBindingUtil.setContentView(this, R.layout.activity_list)
initData()
initRecyclerView()
}
private fun initData() {
for (i in 0..9) {
val userModel = UserModel()
userModel.name = "jack" + 1
userModel.address = "beijing$i"
userModel.age = "age$i"
userModelList?.add(userModel)
}
}
private fun initRecyclerView() {
val layoutManager = LinearLayoutManager(this)
activityListBinding?.recyclerView?.layoutManager = layoutManager
val adapter = UserAdapter()
activityListBinding?.recyclerView?.adapter = adapter
adapter.dataList = userModelList!!
}
}
好了,关于DataBinding的内容就介绍到这里了,当然,实际开发过程中DataBinding使用的远远不止这些,需要我们平时多总结,多思考才能有更多的收获。