Android DataBinding 从入门到进阶
DataBinding 是谷歌官方发布的一个框架,顾名思义即为数据绑定,是 MVVM 模式在 Android 上的一种实现,用于降低布局和逻辑的耦合性,使代码逻辑更加清晰。
DataBinding将布局xml中将控件和数据进行绑定,使数据变化可以驱动控件改变,控件改变可以驱动数据改变。
减少了Activity中对控件的初始化、设置监听、显示数据等操作。
使用databinding你就不需要使用findviewbyid()、setText()等。
MVVM 相对于 MVP,其实就是将 Presenter 层替换成了 ViewModel 层。DataBinding 能够省去我们一直以来的 findViewById() 步骤,大量减少 Activity 内的代码,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常。
视图绑定组件
配置ViewBinding
android {
...
viewBinding {
enabled = true
}
}
通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。在大多数情况下,视图绑定会替代 findViewById。
区别
与findViewById的区别:空安全和类型安全,不存在因引用了一个错误的id而导致的空指针异常或者类型转换异常。
与databinding的区别:databinding仅处理使用 layout代码创建的数据绑定布局;ViewBinding不支持布局变量或布局表达式,因此它不能用于在xml中将布局与数据绑定。
与Android Kotlin Extensions的区别:在使用上,后者简单粗暴,直接id进行访问,而View Binding需要创建绑定类的实例;后者有一些不友好的地方,比如相同的id存在于多个xml,容易导错包,如果包导错了,会有可能别的View用错id导致空指针,而View Binding显然不会有这种情况。
2020年11月11日更新:Android Stuidio 4.1及以上版本,新创建的项目已默认移除kotlin-android-extensions插件。
启用 DataBinding
apply plugin: 'kotlin-kapt'//必须
android {
dataBinding {
enabled = true
}
//AS 4.1之后
bindingFeature{
dataBinding = true
// for view binding :
// viewBinding = true
}
}
就是这么简单,一个简单的databinding配置之后,就可以开始使用数据绑定了。
生成DataBinding布局
我们来看看布局文件该怎么写,首先布局文件不再是以传统的某一个容器作为根节点,而是使用layout作为根节点,在layout节点中我们可以通过data节点来引入我们要使用的数据源。
启用 DataBinding 后,打开原有的布局文件,选中根布局的 根布局,按住 Alt + 回车键,点击 “Convert to data binding layout”,就可以生成 DataBinding 需要的布局规则。
<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_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
和原始布局的区别在于多出了一个 layout 标签将原布局包裹了起来,data 标签用于声明要用到的变量以及变量类型,要实现 MVVM 的 ViewModel 就需要把数据(Model)与 UI(View)进行绑定,data 标签的作用就像一个桥梁搭建了 View 和 Model 之间的通道。
设置基本类型数据
在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。
<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="userName"
type="String" />
<variable
name="age"
type="Integer" />
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:text="@{userName}"
android:textColor="#f00"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(age)}"
android:textColor="#f00"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
MainActivity
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.activity_main)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//setContentView(R.layout.activity_main)
// val binding =DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.userName = "xyh"
binding.age = 20
}
}
设置对象数据
要使用数据绑定,我们得首先创建一个实体类:
data class User(val name:String,val age:Int,val phoneNum:String)
在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。
这里声明了一个 User 类型的变量 user,我们要做的就是使这个变量与TextView 控件挂钩,通过设置 user的变量值同时使 TextView 显示相应的文本。
通过 @{user.name} 使 TextView 引用到相关的变量,DataBinding 会将之映射到相应的 getter 方法。
<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="user"
type="com.example.jetpack.bean.User" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{`名字`+user.name}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{String.valueOf(user.age)}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<TextView
android:id="@+id/tvPhoneNum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{user.phoneNum==null?user.phoneNum:`17817318877`}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAge" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
Activity 中通过 DataBindingUtil 设置布局文件,省略原先 Activity 的 setContentView() 方法。
每个数据绑定布局文件都会生成一个绑定类,ViewDataBinding 的实例名是根据布局文件名来生成,将之改为首字母大写的驼峰命名法来命名,并省略布局文件名包含的下划线。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding=DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
binding.user= User("赵丽颖",20,"17817318859")
}
}
一个简单的dataBinding案例就已经完成。
代码中获取布局文件中的控件
使用binding对象可获取布局文件中的各个对象,根据控件设置的id来获取。如
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
binding.btn.setOnClickListener {
Toast.makeText(this,"点击了按钮",Toast.LENGTH_SHORT).show()
}
在data中定义的variable节点,name属性表示变量的名称,type表示这个变量的类型,实例就是我们实体类的类的全路径。
<data>
<variable
name="user"
type="com.example.jetpack.bean.User" />
data>
1. 自定义 ViewDataBinding 的实例名
可以通过如下方式自定义 ViewDataBinding 的实例名
<data class="CustomBinding">
data>
private val binding: CustomBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.activity_main)
}
2. import
如果 User 类型要多处用到,也可以直接将之 import 进来,这样就不用每次都指明整个包名路径了,而 java.lang.* 包中的类会被自动导入,所以可以直接使用
<data>
<import type="com.example.jetpack.bean.User"/>
<variable
name="user"
type="User" />
data>
可以导入java或kotlin文件中的系统类,比如 import 集合 list:
<import type="java.util.List"/>
3. alias
先使用import节点将User导入,然后直接使用即可。但是如果这样的话又会有另外一个问题,假如我有两个类都是User,这两个UserBean分属于不同的包中,又该如何?这时候就要用到alias了。
在import节点中还有一个属性叫做alias,这个属性表示我可以给该类取一个别名,我给User这个实体类取一个别名叫做Lenve,这样我就可以在variable节点中直接写Lenve了。
<data>
<import type="com.example.jetpack.bean.User" alias="Lenve"/>
<variable
name="user"
type="Lenve" />
data>
4. 字符串拼接
如activity_main中如下属性
android:text="@{`名字`+user.name}"
5. 设置默认值:默认值无需加引号,只在预览视图中显示
由于 TextView在布局文件中并没有明确的值,所以在预览视图中什么都不会显示,不便于观察文本的大小和字体颜色等属性,此时可以为之设定默认值(文本内容或者是字体大小等属性都适用),默认值将只在预览视图中显示,且默认值不能包含引号。
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{`名字`+user.name,default=morenzhi}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
6. 三目运算
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{user.phone==null?user.phone:`147522444`}"
android:textSize="18sp" />
7. 布局中要使用某个类的方法
public class StringUtils {
public static String getNewStr(String str) {
return str+"-new";
}
}
在 data 标签中导入该工具类:
<import type="com.example.jetpack.StringUtils" />
然后就可以像对待一般的函数一样来调用了:
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{StringUtils.getNewStr(userInfo.name)}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
8. 资源引用
dataBinding 支持对尺寸和字符串这类资源的访问。
<string name="title">标题</string>
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{@string/title}"
android:textColor="#f00"
android:textSize="20sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<dimen name="paddingBig">190dp</dimen>
<dimen name="paddingSmall">150dp</dimen>
<string name="format">%s is %s</string>
<data>
<variable
name="flag"
type="boolean" />
data>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="@{flag ? @dimen/paddingBig:@dimen/paddingSmall}"
android:text='@{@string/format("leavesC", "Ye")}'
android:textAllCaps="false" />
9. 属性控制
可以通过变量值来控制 View 的属性:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="可见性变化"
android:visibility="@{user.male ? View.VISIBLE : View.GONE}" />
10. 避免空指针异常
DataBinding 也会自动帮助我们避免空指针异常:
例如,如果 “@{userInfo.password}” 中 userInfo 为 null 的话,userInfo.password会被赋值为默认值 null,而不会抛出空指针异常。
11. 运算符
DataBinding 支持在布局文件中使用以下运算符、表达式和关键字:
目前不支持以下操作:
空合并运算符 ?? 会取第一个不为 null 的值作为返回值
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{user.name ?? user.password}" />
等价于
android:text="@{user.name != null ? user.name : user.password}"
class DemoFrgament : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = DataBindingUtil.inflate<FragmentDemoBinding>(
inflater,
R.layout.fragment_demo,
container,
false
)
//或者
//val binding = FragmentDemoBinding.inflate(inflater, container, false)
return binding.root
}
}
严格意义上来说,事件绑定也是一种变量绑定,只不过设置的变量是回调接口而已,事件绑定可用于以下多种回调事件:
Databinding事件绑定,分两种方式:方法引用和监听绑定,下面分别用案例介绍两种事件绑定的异同。
方式1:直接获取控件设置点击事件
binding.btn1.setOnClickListener {
Toast.makeText(this,"点击了按钮1",Toast.LENGTH_SHORT).show()
}
方式2:方法引用
传入OnClickListener的变量:
<variable
name="listener"
type="android.view.View.OnClickListener" />
在Button中给android:onClick设置listener的变量:
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{listener}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{listener}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn1" />
通过setListener传入点击监听给listener对象:
binding.setListener {
when (it.id) {
R.id.btn1 -> Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show()
R.id.btn2 -> Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show()
}
}
方式3:方法引用
<variable
name="handlers"
type="com.example.jetpack.MainActivity" />
调用语法可以是@{handlers::onClickFriend}或者@{handlers.onClickFriend}:
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers::onClickFriend}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers::onClickFriend}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn1" />
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val url =
"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
binding.user = User("赵丽颖", 20, "17817318859", url)
binding.handlers = this
}
fun onClickFriend(view: View) {
when (view.id) {
R.id.btn1 -> Toast.makeText(this, "点击了按钮1", Toast.LENGTH_SHORT).show()
R.id.btn2 -> Toast.makeText(this, "点击了按钮2", Toast.LENGTH_SHORT).show()
}
}
}
方式4:方法引用
<data>
<variable
name="user"
type="com.example.jetpack.bean.User" />
<variable
name="handlers"
type="com.example.jetpack.MainActivity.MyClickHandlers" />
data>
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="改变name属性"
android:onClick="@{handlers.onClickChangeName}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers::onClickChangAage}"
android:text="改变age属性"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn1" />
在 Activity 内部新建一个类来声明 onClickChangeName() 和 onClickChangAage() 事件相应的回调方法:
class MainActivity : AppCompatActivity() {
lateinit var user: User
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val url =
"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
user = User("赵丽颖", 20, "17817318859", url)
binding.user = user
binding.handlers = MyClickHandlers()
}
inner class MyClickHandlers {
fun onClickChangeName(v: View?) {
user.name = "赵丽颖2"
binding.user=user
}
fun onClickChangAage(v: View?) {
user.age = 18
binding.user=user
}
}
}
方式5:方法引用
把回调方法单独写到一个接口。
<variable
name="handlers"
type="com.example.jetpack.UserClickListener" />
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="按钮"
android:onClick="@{handlers.userClicked}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers::userClicked}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn1" />
interface UserClickListener {
fun userClicked(view: View?)
}
class MainActivity : AppCompatActivity(), UserClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val url =
"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
val user = User("赵丽颖", 20, "17817318859", url)
binding.user = user
binding.handlers = this
}
override fun userClicked(view: View?) {
Toast.makeText(this, "方法引用",Toast.LENGTH_SHORT).show();
}
}
方式6:监听绑定(重要)
onclick="@{()->vm.click()}"
onclick="@{(v)->vm.click(v)}"
onclick="@{()->vm.click(context)}"
onclick="@{BindHelp::staticClick}"
onclick="@{callback}"
将对象直接传回点击方法中。
<variable
name="handlers"
type="com.example.jetpack.MainActivity.MyClickHandlers" />
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{()->handlers.showUser(user)}"
android:text="按钮"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPhoneNum" />
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val url =
"https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
var user = User("赵丽颖", 20, "17817318859", url)
binding.user = user
binding.handlers = MyClickHandlers()
}
inner class MyClickHandlers {
fun showUser(user: User) {
Toast.makeText(this@MainActivity, user.name, Toast.LENGTH_SHORT).show()
}
}
}
DataBinding不支持merge标签。
对于 include 的布局文件,一样是支持通过 dataBinding 来进行数据绑定,此时一样需要在 include 的布局中依然使用 layout 标签,声明需要使用到的变量。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.example.jetpack.bean.User" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{`名字`+user.name}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.example.jetpack.bean.User" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<include
layout="@layout/view_include"
bind:user="@{user}" />
LinearLayout>
layout>
dataBinding 一样支持 ViewStub 布局。
在布局文件中引用 viewStub 布局:
如果需要为 ViewStub 绑定变量值,则 ViewStub 文件一样要使用 layout 标签进行布局,主布局文件使用自定义的 bind 命名空间将变量传递给 ViewStub。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.example.jetpack.bean.User" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ViewStub
android:id="@+id/view_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:user="@{user}"
android:layout="@layout/view_include"/>
LinearLayout>
layout>
获取到 ViewStub 对象,由此就可以来控制 ViewStub 的可见性:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val user = User("赵丽颖", 20, "17817318859")
binding.user = user
//拿到ViewStub的实例之后,调用inflate()方法将隐藏的布局给加载出来
val view = binding.viewStub.viewStub?.inflate()
}
}
如果在 xml 中没有使用 bind:userInfo="@{userInf}"对 ViewStub 进行数据绑定,则可以等到当 ViewStub Inflate 时再绑定变量,此时需要为 ViewStub 设置 setOnInflateListener回调函数,在回调函数中进行数据绑定:
binding.viewStub.viewStub?.setOnInflateListener { stub, inflated ->
//如果在 xml 中没有使用 bind:userInfo="@{userInf}" 对 viewStub 进行数据绑定
//那么可以在此处进行手动绑定
val viewIncludeBinding = DataBindingUtil.bind<ViewIncludeBinding>(inflated)
viewIncludeBinding?.user = user
}
dataBinding 也支持在布局文件中使用数组、Lsit、Set 和 Map,且在布局文件中都可以通过 list[index] 的形式来获取元素。
而为了和 variable 标签的尖括号区分开,在声明 Lsit< String > 之类的数据类型时,需要使用尖括号的转义字符。
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="java.util.List" />
<import type="java.util.Map" />
<import type="java.util.Set" />
<import type="android.util.SparseArray" />
<variable
name="array"
type="String[]" />
<variable
name="list"
type="List<String>" />
<variable
name="map"
type="Map<String, String>" />
<variable
name="set"
type="Set<String>" />
<variable
name="sparse"
type="SparseArray<String>" />
<variable
name="index"
type="int" />
<variable
name="key"
type="String" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".Main7Activity">
<TextView
···
android:text="@{array[1]}" />
<TextView
···
android:text="@{sparse[index]}" />
<TextView
···
android:text="@{list[index]}" />
<TextView
···
android:text="@{map[key]}" />
<TextView
···
android:text='@{map["leavesC"]}' />
<TextView
···
android:text='@{set.contains("xxx")?"xxx":key}' />
LinearLayout>
layout>
DataBinding 提供了 BindingAdapter 这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用。
对于一个 ImageView ,我们希望在某个变量值发生变化时,可以动态改变显示的图片,此时就可以通过 BindingAdapter 来实现。
在java中使用
在java中使用dataBinding展示图片很简单,只需要配置一个静态的BindingAdapter就可以了。
需要先定义一个静态方法,为之添加 BindingAdapter 注解,注解值是为 ImageView 控件自定义的属性名,而该静态方法的两个参数可以这样来理解:
当 ImageView 控件的 url 属性值发生变化时,dataBinding 就会将 ImageView 实例以及新的 url 值传递给 loadImage() 方法,从而可以在此动态改变 ImageView 的相关属性。
public class DataBindingUtils {
@BindingAdapter("imageUrl") //imageUrl:控件的属性名
public static void loadImage(ImageView imageView, String url) {
Glide.with(imageView.getContext())
.load(url)
.placeholder(R.mipmap.ic_launcher)
.error(R.mipmap.ic_launcher)
.into(imageView);
}
}
<ImageView
android:id="@+id/ivNet"
android:layout_width="match_parent"
android:layout_height="300dp"
app:imageUrl="@{user.url}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn" />
当ImageView中使用imageUrl属性时,会自动调用loadImage方法。
public class ImageHelper {
@BindingAdapter({"imageUrl","errorDrawableId","placeDrawableId"})
public static void loadImage(ImageView imageView,String url,int errorDrawableId,int placeDrawableId ){
Glide.with(imageView.getContext())
.load(url)
.error(errorDrawableId)
.placeholder(placeDrawableId)
.into(imageView);
}
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="url"
type="String" />
<variable
name="url2"
type="int" />
data>
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:layout_width="90dp"
android:layout_height="90dp"
android:scaleType="centerCrop"
app:imageUrl="@{url}"
app:placeDrawableId="@{url2}"
app:errorDrawableId="@{url2}"/>
LinearLayout>
layout>
@android.databinding.BindingAdapter(value = {"app:imgUrl", "app:placeholder"}, requireAll = false)
public static void loadImg(ImageView imageView, String url, Drawable placeholder) {
GlideApp.with(imageView)
.load(url)
.placeholder(placeholder)
.into(imageView);
}
这里 requireAll = false 表示我们可以使用这两个两个属性中的任一个或同时使用,如果requireAll = true 则两个属性必须同时使用,不然报错。默认为 true。
在kotlin中使用
kotlin需要加@JvmStatic注解
首先:kotlin中没有static关键字,但是提供了companion object{}代码块和使用object关键字。
object关键字声明一种特殊的类,这个类只有一个实例,因此看起来整个类就好像是一个对象一样,这里把类声明时的class关键字改成了object,这个类里面的成员默认都是static的。
@JvmStatic注解:与伴生对象搭配使用,将变量和函数声明为真正的JVM静态成员。
要加上kapt插件:
apply plugin: 'kotlin-kapt'
class DataBindingUtils {
companion object {
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(view: ImageView, url: String) {
Glide.with(view.context).load(url).into(view)
}
}
}
object DataBindingUtils {
//imageUrl:就是要在布局文件中使用的属性
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(view: ImageView, url: String) {
Glide.with(view.context).load(url).into(view)
}
}
val url="https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796319594,2802761532&fm=26&gp=0.jpg"
binding.user= User("赵丽颖",20,"17817318859",url)
<ImageView
android:id="@+id/ivNet"
android:layout_width="match_parent"
android:layout_height="300dp"
app:imageUrl="@{user.url}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn" />
BindingAdapter 更为强大的一点是可以覆盖 Android 原先的控件属性。
例如,可以设定每一个 TextView 的文本都要加上后缀:“-赵丽颖”
@BindingAdapter("android:text")
public static void setText(TextView view, String text) {
view.setText(text + "赵丽颖");
}
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{`名字`+user.name,default=morenzhi}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
这样,整个工程中使用到了 “android:text” 这个属性的控件,其显示的文本就会多出一个后缀-赵丽颖。
可以用这个属性来加载本地图片:
public class ImageViewAdapter {
@BindingAdapter("android:src")
public static void setSrc(ImageView view, Bitmap bitmap) {
view.setImageBitmap(bitmap);
}
@BindingAdapter("android:src")
public static void setSrc(ImageView view, int resId) {
view.setImageResource(resId);
}
@BindingAdapter("imageUrl")
public static void setSrc(ImageView imageView, String url) {
Glide.with(imageView.getContext()).load(url)
.placeholder(R.mipmap.ic_launcher)
.into(imageView);
}
@BindingAdapter({"app:imageUrl", "app:placeHolder", "app:error"})
public static void loadImage(ImageView imageView, String url, Drawable holderDrawable, Drawable errorDrawable) {
Glide.with(imageView.getContext())
.load(url)
.placeholder(holderDrawable)
.error(errorDrawable)
.into(imageView);
}
}
object DataBindingUtils {
/**
* View的显示和隐藏
*/
@BindingAdapter("isGone")
@JvmStatic
fun bindISGone(view: View, isGone: Boolean) {
view.visibility = if (isGone) View.GONE else View.VISIBLE
}
}
<data>
<variable
name="hasPlantings"
type="boolean" />
data>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/garden_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingLeft="@dimen/margin_normal"
android:paddingRight="@dimen/margin_normal"
app:isGone="@{!hasPlantings}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/list_item_garden_planting" />
<TextView
android:id="@+id/empty_garden"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/garden_empty"
android:textSize="24sp"
app:isGone="@{hasPlantings}" />
dataBinding 还支持对数据进行转换,或者进行类型转换。
与 BindingAdapter 类似,以下方法会将布局文件中所有以@{String}方式引用到的String类型变量加上后缀-conversionString。
@BindingConversion
public static String conversionString(String text) {
return text + "-conversionString";
}
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text='@{"xxx"}'
android:textAllCaps="false"/>
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{userInfo.name}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
如果同时设置了BindingAdapter 和 BindingConversion ,都会同时生效了,而 BindingConversion 的优先级要高些:
@BindingConversion
public static String conversionString(String text) {
return text + "-conversionString";
}
@BindingAdapter("android:text")
public static void setText(TextView view, String text) {
view.setText(text + "-TextView");
}
此外,BindingConversion 也可以用于转换属性值的类型
看以下布局,此处在向 background 和 textColor 两个属性赋值时,直接就使用了字符串,按正常情况来说这自然是会报错的,但有了 BindingConversion 后就可以自动将字符串类型的值转为需要的 Drawable 和 Color 了。
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:background="红色"
android:textColor="蓝色"
android:layout_height="wrap_content"/>
@BindingConversion
public static Drawable convertStringToDrawable(String str) {
if (str.equals("红色")) {
return new ColorDrawable(Color.parseColor("#FF4081"));
}
if (str.equals("蓝色")) {
return new ColorDrawable(Color.parseColor("#3F51B5"));
}
return new ColorDrawable(Color.parseColor("#344567"));
}
@BindingConversion
public static int convertStringToColor(String str) {
if (str.equals("红色")) {
return Color.parseColor("#FF4081");
}
if (str.equals("蓝色")) {
return Color.parseColor("#3F51B5");
}
return Color.parseColor("#344567");
}
适配扩展支持
binding
的函数。
BindingMethods包含若干BindingMethod,BindingMethod是BindingMethods的子集。
indingMethods与BindingMethod用于类的注解,简单的可以理解为,定义xml中定义的属性与某个medthod(方法)绑定。
//这里BindMethods可以放在任何的类上面,重点在于内部属性声明
@BindingMethods(
BindingMethod(
type = AppCompatImageView::class,
attribute = "image",
method = "setImageDrawable"
)
)
object BdTool {
@JvmStatic
fun getTitle(type: String): String {
return "type:$type"
}
}
<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="imgRes"
type="android.graphics.drawable.Drawable" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatImageView
image="@{imgRes}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.apply {
imgRes=getDrawable(R.mipmap.ic_launcher)
}
}
}
通过DataBinding进行绑定控件以及进行相关操作,但是,这遇到了一个瓶颈,就比如绑定的变量发生变化的时候,每次都要重新向 ViewDataBinding 传值进行更新操作之后才能刷新UI。那么怎么就能自动刷新UI了呢?那就得用 单向绑定 了!
实现数据变化自动驱动 UI 刷新的方式有三种:
一个纯净的 ViewModel 类被更新后,并不会让 UI 自动更新。而数据绑定后,我们自然会希望数据变更后 UI 会即时刷新,Observable 就是为此而生的概念。
BaseObservable 提供了 notifyChange() 和 notifyPropertyChanged() 两个方法,前者会刷新所有的值域,后者则只更新对应 BR 的 flag,该 BR 的生成通过注释 @Bindable 生成,可以通过 BR notify 特定属性关联的视图。
package com.example.jetpack.bean;
import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;
import com.example.jetpack.BR;
/**
* Cerated by xiaoyehai
* Create date : 2020/11/14 13:02
* description :
*
* 1.Student继承BaseObservable
* 2.每个getter()加上注解@Bindable
* 3.每个setter()加上notifyPropertyChanged(BR.xxx); - BR在rebuild后自动生成
*/
public class UserInfo extends BaseObservable {
// 如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
private String name;
//如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
private String password;
private String desc;
public UserInfo(String name, String password, String desc) {
this.name = name;
this.password = password;
this.desc = desc;
}
@Bindable
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
//只更新本字段:name变化时只会更新新本字段
notifyPropertyChanged(BR.name);
}
@Bindable
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
//更新所有字段:password会更新所有字段
notifyChange();
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
BR是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable标记过 getter方法会在BR中生成一个静态常量。
UserInfo用kotlin来实现:
class UserInfo : BaseObservable() {
//由于kotlin的属性默认是public修饰,所以可以直接在属性上@Bindable, 如何设置了修饰符且不为public的话,
// 则可使用@get BIndable(表示在get()方法上标记@Bindable)
// 对name进行@Bindable标志,然后会生成BR.name
@Bindable
var name: String = ""
set(value) {
field = value
// 当name,发生改变时只会刷新与name相关控件的值,不会刷新其他的值
notifyPropertyChanged(BR.name)
}
@get: Bindable
var password: String = ""
set(value) {
field = value
// 当password 发生改变时,也会刷新其他属性相关的控件的值
notifyChange()
}
var desc: String = ""
}
<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="userInfo"
type="com.example.jetpack.bean.UserInfo" />
<variable
name="handlers"
type="com.example.jetpack.MainActivity.MyClickHandlers" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{userInfo.name}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{userInfo.password}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<TextView
android:id="@+id/tvDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{userInfo.desc}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPassword" />
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers.onClickChangeName}"
android:text="改变name和desc"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDesc" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers.onClickChangePassword}"
android:text="改变password和desc"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn1" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
class MainActivity : AppCompatActivity() {
lateinit var userInfo: UserInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
userInfo = UserInfo("赵丽颖1", "1234561","描述1")
binding.userInfo = userInfo
binding.handlers=MyClickHandlers()
}
inner class MyClickHandlers {
fun onClickChangeName(v: View?) {
userInfo.name = "赵丽颖2"
userInfo.desc = "描述2"
}
fun onClickChangePassword(v: View?) {
userInfo.name = "赵丽颖3"
userInfo.password = "123456123"
userInfo.desc = "描述3"
}
}
}
可以看到,name 值的改变没有同时刷新 desc ,而 password 刷新的同时也刷新了name 和 price 。
OnPropertyChangedCallback:属性变更监听
实现了 Observable 接口的类允许注册一个监听器,当可观察对象的属性更改时就会通知这个监听器,此时就需要用到 OnPropertyChangedCallback。
当中 propertyId 就用于标识特定的字段
userInfo.addOnPropertyChangedCallback(object :Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
when(propertyId) {
BR._all-> Log.e("xyh", "BR._all")
BR.name-> Log.e("xyh", "BR.name")
BR.password-> Log.e("xyh", "BR.password")
}
}
})
ObervablueFields,它其实也是为了能够减少代码量,当一个Bean没有那么多属性的时候,我们不需要写这么多的get和set方法,使用ObervablueFields只需要通过两行代码即可实现相同效果。
继承于 Observable 类相对来说限制有点高,且也需要进行 notify 操作,因此为了简单起见可以选择使用 ObservableField。ObservableField 可以理解为官方对 BaseObservable 中字段的注解和刷新等操作的封装,官方原生提供了对基本数据类型的封装,例如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble 以及 ObservableParcelable ,也可通过 ObservableField泛型来申明其他类型。
public class UserInfo {
private ObservableField<String> name;
private ObservableInt age;
private ObservableField<String> desc;
public UserInfo(ObservableField<String> name, ObservableInt age, ObservableField<String> desc) {
this.name = name;
this.age = age;
this.desc = desc;
}
public ObservableField<String> getName() {
return name;
}
public void setName(ObservableField<String> name) {
this.name = name;
}
public ObservableInt getAge() {
return age;
}
public void setAge(ObservableInt age) {
this.age = age;
}
public ObservableField<String> getDesc() {
return desc;
}
public void setDesc(ObservableField<String> desc) {
this.desc = desc;
}
}
对 ObservableField属性值的改变都会立即触发 UI 刷新,概念上与 Observable 区别不大。
class MainActivity : AppCompatActivity() {
lateinit var userInfo: UserInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val name = ObservableField<String>("赵丽颖1")
val age = ObservableInt(20)
val desc = ObservableField("描述")
userInfo = UserInfo(name, age, desc)
binding.userInfo = userInfo
binding.handlers = MyClickHandlers()
}
inner class MyClickHandlers {
fun onClickChangeName(v: View?) {
userInfo.name.set("赵丽颖2")
userInfo.age.set(21)
userInfo.desc.set("描述2")
}
fun onClickChangePassword(v: View?) {
userInfo.name.set("赵丽颖3")
}
}
}
上面讲到 ObservableField 单向绑定,和 BaseObservable 相比之下,ObservableField 简单了许多,只需要用它的方法即可,提供了自动刷新UI的方法。
如果用到 Map 或者是 List,同样 DataBinding 还提供了 ObservableMap 和 ObservableList,dataBinding 也提供了包装类用于替代原生的 List 和 Map,分别是 ObservableList 和 ObservableMap,当其包含的数据发生变化时,绑定的视图也会随之进行刷新。
下面来基本使用一下吧!
<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>
<import type="androidx.databinding.ObservableList" />
<import type="androidx.databinding.ObservableMap" />
<variable
name="list"
type="ObservableList<String>" />
<variable
name="map"
type="ObservableMap<String,String>" />
<variable
name="index"
type="int"/>
<variable
name="key"
type="String"/>
<variable
name="handlers"
type="com.example.jetpack.MainActivity.MyClickHandlers" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{list.get(index)}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{list.get(1)}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<TextView
android:id="@+id/tvDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{map[key]}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPassword" />
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{handlers.onClickChange}"
android:text="改变数据"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDesc" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
class MainActivity : AppCompatActivity() {
var list: ObservableList<String> = ObservableArrayList()
var map: ObservableArrayMap<String, String> = ObservableArrayMap()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
list.add("赵丽颖1")
list.add("赵丽颖2")
binding.list = list
binding.index = 0
map["name"] = "赵丽颖3"
map["age"] = "21"
binding.map = map
binding.key = "name"
binding.handlers = MyClickHandlers()
}
inner class MyClickHandlers {
fun onClickChange(v: View?) {
list[0]="zly1"
map["name"] = "zly2"
}
}
}
Databinding是Google推出的一个支持View与ViewModel绑定的Library,可以说Databinding建立了一个UI与数据模型之间的桥梁,即UI的变化可以通知到ViewModel, ViewModel的变化同样能够通知到UI从而使UI发生改变,大大减少了之前View与Model之间的胶水代码,如findViewById;改变和获取TextView的内容还需要调用setText()、 getText(),获取EditText编辑之后的内容需要调用getText(),而有了Databinding的双向绑定,这些重复的工作都将被省去。下面我们就来看一下如何使用Databinding来双向绑定。
双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据。
现在要想到,在哪那些方面需要双向绑定,哪那些方面不需要双向绑定,生存还是死亡,这是个问题。
在以上三个单向绑定案例中,貌似双向绑定没多大用处,下面举例一种情况,在输入账号和密码的时候,UI更新同时数据也要更新,这就用到双向绑定,总的来说,双向绑定使用还是不算多的,双向绑定是安卓MVVM架构的基础。
Data Binding本身是不支持双向绑定的,我们想要实现双向绑定首先要改造一下实体类。让实体类继承BaseObservable。
目前双向绑定仅支持如text,checked,year,month,hour,rating,progress等绑定。
假设有一种情况,当我们在EditText里面输入内容的时候,如果此时我们的User已经和EditText关联,那么我们希望当输入框内容改变的时候,User对应的字段也发生变化,反之User发生变化的时候,输入框的内容也会跟着变化。这也是MVVM架构的思想,有了databinding框架,就可以帮我们快速实现一个MVVM架构。
看以下例子,当 EditText 的输入内容改变时,会同时同步到变量 userInfo,绑定变量的方式比单向绑定多了一个等号:android:text=“@={userInfo.name}”
双向绑定语法:
主要就是在布局文件中EditText中加上了一个 = (等于)号。
<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="userInfo"
type="com.example.jetpack.bean.UserInfo" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/et"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={userInfo.name}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{userInfo.name}"
android:textSize="16sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
val user = UserInfo("赵丽颖", 20)
binding.userInfo = user
数据源可以使用使用 继承BaseObservable 方式:
public class UserInfo extends BaseObservable {
//如果是 public 修饰符,则可以直接在成员变量上方加上 @Bindable 注解
//如果是 private 修饰符,则在成员变量的 get 方法上添加 @Bindable 注解
private String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
@Bindable
public String getName() {
return name;
}
@Bindable
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
//只更新本字段
notifyPropertyChanged(BR.name);
//更新所有字段
//notifyChange();
}
public void setAge(int age) {
this.age = age;
notifyPropertyChanged(BR.age);
}
}
数据源也可以使用 ObservableField 方式,来更好的更新UI,使用方式看上面的单向绑定中使用ObservableField 。
双向绑定之基于InverseBindingAdapter的反向绑定
activity_main.xml
<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="adapter"
type="com.example.jetpack_demo.RvAdapter" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
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"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="10"
tools:listitem="@layout/item_rv" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
item.rv.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="userBean"
type="com.example.jetpack_demo.UserInfo" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{userBean.name}"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(userBean.age)}"
android:textSize="20sp" />
LinearLayout>
layout>
UserInfo
data class UserInfo(val name:String,val age:Int)
MainActivity
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
DataBindingUtil.setContentView(this, R.layout.activity_main)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.recyclerView.addItemDecoration(
DividerItemDecoration(
this,
DividerItemDecoration.VERTICAL
)
)
var datas = mutableListOf<UserInfo>()
for (i in 0..29) {
datas.add(UserInfo("zly$i", 10 + i))
}
val rvAdapter = RvAdapter(this, datas)
binding.recyclerView.adapter = rvAdapter
}
}
RvAdapter
package com.example.jetpack;
/**
* Cerated by xiaoyehai
* Create date : 2020/11/14 15:53
* description :
*/
public class RvAdapter extends RecyclerView.Adapter<RvAdapter.ViewHolder> {
private Context mContext;
private List<UserInfo> mDatas;
public RvAdapter(Context context, List<UserInfo> datas) {
mContext = context;
this.mDatas = datas;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
//用父类ViewDataBinding接收也可以,不过使用的时候需要强转
/*ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext),
R.layout.item_rv, parent, false);*/
ItemRvBinding binding = DataBindingUtil.inflate(LayoutInflater.from(mContext),
R.layout.item_rv, parent, false);
//设置点击事件
binding.getRoot().setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
/*ViewDataBinding binding = holder.getBinding();
binding.setVariable(BR.userBean, mDatas.get(position));//绑定数据
binding.executePendingBindings();//立刻执行绑定,执行刷新*/
ItemRvBinding binding = holder.getBinding();
binding.setUserBean(mDatas.get(position));
binding.tvName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
binding.executePendingBindings();//立刻执行绑定,执行刷新
}
@Override
public int getItemCount() {
return mDatas.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ItemRvBinding mDataBinding;
public ViewHolder(ItemRvBinding binding) {
super(binding.getRoot());
mDataBinding = binding;
}
public ItemRvBinding getBinding() {
return mDataBinding;
}
}
}
RvAdapter Kotlin写法:
class RvAdapter(private val context: Context, private val data: List<UserInfo>) :
RecyclerView.Adapter<RvAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: ItemRvBinding =
DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.item_rv,
parent,
false
)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.userBean = data[position]
//holder.binding.setVariable(BR.userBean,data[position])
holder.binding.executePendingBindings() //立刻执行绑定,执行刷新
}
override fun getItemCount(): Int = data.size
class ViewHolder(var binding: ItemRvBinding) : RecyclerView.ViewHolder(binding.root)
}
```xml
<?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>
<import type="com.xiaoyehai.mvvm.adapter.LvAdapter" />
<variable
name="adapter"
type="LvAdapter" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.LvActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{adapter}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
条目布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="user"
type="com.xiaoyehai.mvvm.entity.UserBean" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ImageView
android:layout_width="100dp"
app:imageUrl="@{user.imgUrl}"
android:layout_height="100dp" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@{user.name}"
android:textSize="20sp" />
LinearLayout>
layout>
/**
* Created by : xiaoyehai
* Create date : 2019/10/14 14:21
* description :
*/
public class LvAdapter extends BaseAdapter {
private Context mContext;
private List<UserBean> mDatas;
private int mLayoutId;
private final LayoutInflater mLayoutInflater;
public LvAdapter(Context context, List<UserBean> datas, int layoutId) {
mContext = context;
this.mDatas = datas;
this.mLayoutId = layoutId;
mLayoutInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public Object getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemLvBinding binding;
ViewHolder holder;
if (convertView == null) {
//获取item布局的binding
binding = DataBindingUtil.inflate(mLayoutInflater, mLayoutId, parent, false);
//获取布局
convertView = binding.getRoot();
holder = new ViewHolder();
//缓存binding到holder
holder.setItemLvBinding(binding);
//设置Tag
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
binding = (ItemLvBinding) holder.getItemLvBinding();
}
binding.setUser(mDatas.get(position));
return convertView;
}
/**
* viewholder类里只有一个binding对象和它的get,set方法
*/
private class ViewHolder {
private ViewDataBinding binding;
public void setItemLvBinding(ViewDataBinding binding) {
this.binding = binding;
}
public ViewDataBinding getItemLvBinding() {
return binding;
}
}
}
DataBinding使用了Gradle插件+APT技术,我们build项目时DataBinding会生成多个文件。
首先来看看ActivityMainBinding是什么时间生成的?
回顾一下,在activity_main.xml中把根布局改为layout标签之后,在回到对应的MainActivity就可以使用ActivityMainBinding对象了。这一点也可以得到证实:在引入layout之后,build项目会发现在Project目录下生成多个新的文件,目录大致如下:
此外,DataBinding还将原有的activity_main.xml文件进行了拆分,分别是activity_mian.xml和activity_main-layout.xml。
原有的activity_main.xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.zly.ktdemo.User" />
data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="50dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={user.name}" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(user.age)}" />
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
LinearLayout>
layout>
拆分后的activity_mian.xml(Android OS 渲染的布局文件):
通过上面的代码我们发现DataBinding将原有的layout和data标签去除了。并为根布局声明了一个layout/文件名_0的tag,为其他使用到@{}或@={}的控件按顺序添加了一个binding_X的tag。
拆分后的activity_main-layout.xml(DataBinding需要的布局控件信息):
//我们声明的全局变量
<Variables name="user" declared="true" type="com.zly.ktdemo.User">
<Target tag="binding_1" view="EditText"> //tag对应的View类型
<Expressions>
//控件绑定具体属性和Model中的具体属性
<Expression attribute="android:text" text="user.name">
<Location endLine="19" endOffset="39" startLine="19" startOffset="12" />
<TwoWay>true</TwoWay> //是否是双向的
<ValueLocation endLine="19" endOffset="37" startLine="19" startOffset="29" />
</Expression>
</Expressions>
<location endLine="19" endOffset="42" startLine="16" startOffset="8" />
</Target>
结合生成文件和XML的位置和时间节点,大致可以看出生成的原理是Gradle插件+APT,这个插件是Gradle内置的,目前还没有查找到相关插件的实现在哪里。
val binding =
DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
通过activity的setContentView加载布局,并通过window找到id为content的ViewGroup,它是一个FrameLayout用于加载我们添加的布局文件。接下来就是bindToAddedViews方法。
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);
}
}
public class DataBindingUtil {
private static DataBinderMapper sMapper = new DataBinderMapperImpl();
private static DataBindingComponent sDefaultComponent = null;
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId) {
return setContentView(activity, layoutId, sDefaultComponent);
}
public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
int layoutId, @Nullable DataBindingComponent bindingComponent) {
activity.setContentView(layoutId);
View decorView = activity.getWindow().getDecorView();
ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
}
}
parent中的子View就是我们布局文件中的根布局LinearLayout,所以走的是if中的代码
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
}
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
先看看大致的调用关系,找到最终绑定View的地方:
setContentView
----->DataBinderMapperImpl#getDataBinder()
----->根据根布局标记的tag ActivityMainBindingImpl
----->内部构造方法调用invalidateAll();
----->mRebindRunnablemRebindRunnable
---->executePendingBindings()
---->executeBindingsInternal()
---->executeBindings()
数据绑定的具体实现是在ActivityMainBindingImpl#executeBindings()方法中:
//绑定的数据一旦变化,就会调进这个方法
//我们也可以调用自动生成的XXXBinding.executePendingBindings(),主动调入该函数来刷新UI
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String userName = null;
int userAge = 0;
com.zly.ktdemo.User user = mUser;
java.lang.String stringValueOfUserAge = null;
if ((dirtyFlags & 0x7L) != 0) {
if (user != null) {
// read user.name
userName = user.getName();
}
if ((dirtyFlags & 0x5L) != 0) {
if (user != null) {
// read user.age
userAge = user.getAge();
}
// read String.valueOf(user.age)
stringValueOfUserAge = java.lang.String.valueOf(userAge);
}
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, com.zly.ktdemo.DataBindingUtils.conversionString(userName));
}
if ((dirtyFlags & 0x4L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, mboundView1androidTextAttrChanged);
}
if ((dirtyFlags & 0x5L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, com.zly.ktdemo.DataBindingUtils.conversionString(stringValueOfUserAge));
}
}
这里通过TextViewBindingAdapter.setText()去给UI控件赋值;而且在赋值之前会对model对象进行判空,这样就避免了set XX()方法时出现空指针异常。
到这里还没完,这个地方只能算是完成数据到View的映射,当我们更改Model的ObservableField属性去更新数据的时候,又是如何更新UI的呢?
ObservableField是对常用数据结构对包装类,它最终继承BaseObservable,它内部封装了观察者模式,可以监听数据的变化。
看一下它的set方法:
/**
* Set the stored value.
*/
public void set(T value) {
if (value != mValue) {
mValue = value;
notifyChange();
}
}
这里notifyChange()之后会通过观察者模式的OnPropertyChangedCallback回调到ViewDataBinding #WeakListListener#onChanged()。
private static class WeakListListener extends ObservableList.OnListChangedCallback
implements ObservableReference<ObservableList> {
final WeakListener<ObservableList> mListener;
public WeakListListener(ViewDataBinding binder, int localFieldId) {
mListener = new WeakListener<ObservableList>(binder, localFieldId, this);
}
@Override
public WeakListener<ObservableList> getListener() {
return mListener;
}
@Override
public void addListener(ObservableList target) {
target.addOnListChangedCallback(this);
}
@Override
public void removeListener(ObservableList target) {
target.removeOnListChangedCallback(this);
}
@Override
public void onChanged(ObservableList sender) {
ViewDataBinding binder = mListener.getBinder();
if (binder == null) {
return;
}
ObservableList target = mListener.getTarget();
if (target != sender) {
return; // We expect notifications only from sender
}
binder.handleFieldChange(mListener.mLocalFieldId, target, 0);
}
然后
handleFieldChange
—>requestRebind
---->executePendingBindings()
---->executeBindingsInternal()
---->executeBindings()
最终又调用到了最终调用了executeBindings(),这里就是完成UI更新的地方。
再次回到更新UI的地方ActivityMainBindingImpl#executeBindings()。
androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.mboundView1,
(androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged) null,
(androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged) null,
(androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged) null,
mboundView1androidTextAttrChanged);
看一下这个setTextWatcher()方法,当数据发生变化的时候,TextWatcher在回调onTextChanged()的最后,会通过回调传入的ActivityMainBindingImpl # mboundView1androidTextAttrChanged # onChange()
private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of user.name
// is user.setName((java.lang.String) callbackArg_0)
java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
// localize variables for thread safety
// user.name
java.lang.String userName = null;
// user != null
boolean userJavaLangObjectNull = false;
// user
com.zly.ktdemo.User user = mUser;
userJavaLangObjectNull = (user) != (null);
if (userJavaLangObjectNull) {
user.setName(((java.lang.String) (callbackArg_0)));
}
}
};
这里就比较简单了,获取控件中最新的值,然后给ObservableField属性赋值。