赞
踩
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 需要的布局规则。
<?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_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表示这个变量的类型,实例就是我们实体类的类的全路径。
<?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="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" /> <!--int 要转为string,使用拼接,或者valueOf,toString都可以。--> <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<ActivityMainBinding>(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 方法。
<?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="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" /> <!--注意:这里age是int类型,必须转化为String,否则会运行时异常--> <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 支持在布局文件中使用以下运算符、表达式和关键字:
目前不支持以下操作:
此外,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 标签,声明需要使用到的变量。
<?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"> <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>
<?xml version="1.0" encoding="utf-8"?> <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。
<?xml version="1.0" encoding="utf-8"?> <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 > 之类的数据类型时,需要使用尖括号的转义字符。
<?xml version="1.0" encoding="utf-8"?> <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);
}
}
<?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"> <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"
}
}
<?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="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 : * <p> * 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 = "" }
<?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="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,当其包含的数据发生变化时,绑定的视图也会随之进行刷新。
下面来基本使用一下吧!
<?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="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中加上了一个 = (等于)号。
<?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="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
<?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="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" /> <!-- app:adapter="@{adapter}"--> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
item.rv.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"> <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>
条目布局:
<?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"> <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:
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <!--DataBinding的配置文件--> <data> <!--name属性相当于声明了一个全局属性、type指定name对应的具体的数据类或是MVVM中VM--> <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属性赋值。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。