前言
Data binding 库是官方推出的数据绑定框架,允许在 xml 中指定 UI 组件的数据来源。最低支持 Android 4.1((API level 14)或更高版本的设备。在 app 下的 build.gradle
添加以下依赖就可以使用 Data binding
android {
...
dataBinding {
enabled = true
}
}
入门
- 首先定义一个实体类
User
data class User(val firstName: String?, val lastName: String, val age: Int) {
}
- 接下来在布局中,把
User
类属性 firstName 绑定到一个 TextView 上
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="user"
type="com.i3kmsteady.databindingsample.entity.User" />
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Hello Databinding !" />
</android.support.constraint.ConstraintLayout>
</layout>
- 选中根布局,使用
Alt + Enter
键,可以迅速的将一个普通布局,转变为一个 data binding 布局。以layout
标签开头,后面跟data
元素 和 视图元素。
可以在data
元素中中定义变量,如user
,接下来在视图元素中就可以通过@{}
引用这个变量。
在 xml 中,推荐使用 tools:text="默认值" 的方式进行布局调试
- 在 Activity 中进行数据的绑定
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityBasicBinding = DataBindingUtil.setContentView(this, R.layout.activity_basic)
binding.user = User("steady", "13km", 24)
}
默认情况下,Data binding 根据布局文件的名称再加上 binding
后缀生成绑定类,通过绑定类给给在 data
中声明的变量赋值即可
Data binding 表达式语言
- 可以在表达式中使用以下操作符和关键字
Mathematical + - / * %
String concatenation +
Logical && ||
Binary & | ^
Unary + - ! ~
Shift >> >>> <<
Comparison == > < >= <=
instanceof
Grouping ()
Literals - character, String, numeric, null
Cast
Method calls
Field access
Array access []
Ternary operator ?:
示例:
android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'
- 不支持的操作符
this
super
new
表达式的基础使用
- 使用
<
操作符,要使用转义符<
。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable name="user" type="com.i3kmsteady.databindingsample.entity.User"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ExpressionLanguageActivity">
<!-- < 表示:"<", > 表示:">" -->
<android.support.v7.widget.AppCompatImageView
android:visibility="@{user.age <25 ? View.VISIBLE: View.GONE}"
android:src="@mipmap/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>
否则会出现以下错误
> Error: 与元素类型 "android.support.v7.widget.AppCompatImageView" 相关联的 "android:visibility" 属性值不能包含 '<' 字符
除了 java.lang.* 包会自动导入,其他类都需要在 data
元素中手动导入。比如在表达式中根据条件去判断 View 的隐藏或显示 ,没有手动导入 View 包,会出现以下错误:
****/ data binding error ****msg:Identifiers must have user defined types from the XML file. View is missing it
file:G:\WYJ\test\DatabindingSample\app\src\main\res\layout\activity_expressionlanguage.xml
loc:13:49 - 13:52
- 空安全操作符
??
<!-- ?? 空安全操作符 -->
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tvNull"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{user.lastName ?? user.firstName}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView"
tools:text="空安全操作符" />
如果表达式左侧的值为 null,则显示右侧的值。生成的绑定类,会自动检查空值并避免空指针异常
- 资源引用
Data binding 表达式,也提供了对资源文件的引用。
- 定义一个字符串资源
<resources>
<string name="source_reference">My name is %1$s</string>
</resources>
- xml 中通过表达式去引用
<!-- 资源引用 -->
<android.support.v7.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@{@string/source_reference(user.firstName)}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvFixed"
tools:text="资源引用" />
- import
- 在
data
元素中,手动导入 View 类或者其他,就可以在表达式中引用导入的类。如通过表达式去控制 View 的显示与隐藏<?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="android.view.View" /> <import type="com.i3kmsteady.databindingsample.entity.User" /> <variable name="user" type="User" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.AppCompatImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:src="@mipmap/ic_launcher" android:visibility="@{user.age < 13 ? View.VISIBLE:View.GONE}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>
- 在表达式中,也可以很方便的使用工具类来处理一些逻辑
fun toUpperCase(str:String):String{ return str.toUpperCase() }
<?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.i3kmsteady.databindingsample.utils.MyStringUtilsKt" /> <import type="com.i3kmsteady.databindingsample.entity.User" /> <variable name="user" type="User" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 使用工具类 --> <android.support.v7.widget.AppCompatTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="@{MyStringUtilsKt.toUpperCase(user.firstName)}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/tvResourceReference" tools:text="使用工具类" /> </android.support.constraint.ConstraintLayout> </layout>
-
别名
当导入的类,名称发生冲突时,可以使用别名来代替
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/>
在表达式中,就可以使用
Vista
来引用自己定义的类 -
Includes
- 定义一个 include 布局
<?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.i3kmsteady.databindingsample.entity.User" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <android.support.v7.widget.AppCompatTextView android:layout_width="0dp" android:layout_height="30dp" android:background="@color/colorPrimary" android:gravity="center" android:text="@{user.firstName}" android:textColor="@android:color/white" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" tools:text="Include " /> </android.support.constraint.ConstraintLayout> </layout>
- 通过 include 元素引用
<!-- Include --> <include layout="@layout/layout_include" android:layout_width="0dp" android:layout_height="30dp" android:layout_marginTop="10dp" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvUtils" app:user="@{user}" />
- 需要注意的是,Data binding 不支持 include 作为
merge
元素的直接子元素。如:<?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"> <data> <variable name="user" type="com.example.User"/> </data> <merge><!-- Doesn't work --> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </merge> </layout>
- 定义一个 include 布局
笔者在使用 include
时,曾遇到一个小问题。我把界面 finish()
定义在基类中,但传入的是子类对象。在 include
布局中,声明导入的类型是基类的。在绑定 include
标签时,需要做一次强转,否则编译不通过。
事件绑定
Data binding 允许在表达式中,处理 View 的一些事件(如: onClick())。提供了两种方式来处理
- Method references
-
首先定义
MyHandlers
类class MyHandlers { fun onClickFriend(view: View) { Toast.makeText(view.context, "方法引用", Toast.LENGTH_LONG).show() } }
-
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="user" type="com.i3kmsteady.databindingsample.entity.User" /> <variable name="handlers" type="com.i3kmsteady.databindingsample.MyHandlers" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.EventActivity"> <Button android:id="@+id/btnMethodReferences" android:layout_width="0dp" android:layout_height="wrap_content" android:onClick="@{handlers::onClickFriend}" android:text="@{user.firstName}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> </android.support.constraint.ConstraintLayout> </layout>
-
当点击 Button 的时候,就可以正常的显示 Toast 了。关于
Method references
,会在编译期间进行参数的检查, 如果监听器的参数和表达式调用的方法参数不一致,编译将会出错Listener class android.view.View.OnClickListener with method onClick did not match signature of any method handlers::onClickFriend
- Listener bindings
Listener bindings
是在事件发生时,执行对应的表达式。和 Method references
不同的是,可以运行任意的表达式,对参数没有要求。
- 定义
Presenter
类class Presenter(val context: Context) { fun onSaveClick(task: Task) { Toast.makeText(context, task.toString(), Toast.LENGTH_LONG).show() } }
- 布局引用
<?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.i3kmsteady.databindingsample.entity.User" /> <variable name="task" type="com.i3kmsteady.databindingsample.entity.Task" /> <variable name="presenter" type="com.i3kmsteady.databindingsample.Presenter" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.EventActivity"> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:onClick="@{() -> presenter.onSaveClick(task)}" android:text="@{user.lastName}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/btnMethodReferences" /> </android.support.constraint.ConstraintLayout> </layout>
- 相比于
Method references
,Listener bindings
的处理更加灵活,方法也不受参数的限制。
官方建议在 xml 中,表达式应该简练,避免复杂的逻辑操作。推荐在表达式的回调中,去处理复杂的逻辑。
Observable
Data binding 支持当一个对象的属性发生改变,自动去刷新 UI 。下面简单介绍下 BaseObservable 的用法
-
定义一个类,继承自
BaseObservable
,在 get 方法上添加 @Bindable 注解。class Person : BaseObservable() { @get:Bindable var firstName: String = "" set(value) { field = value notifyPropertyChanged(BR.firstName) } @get:Bindable var lastName: String = "" set(value) { field = value notifyPropertyChanged(BR.firstName) } }
-
通过点击按钮,去修改
Person
的 firstName 属性值class Presenter(val context: Context) { fun onSaveClick(task: Task) { Toast.makeText(context, task.toString(), Toast.LENGTH_LONG).show() } fun updateName(person: Person) { person.firstName = "Kotlin: " + Random.nextInt() } }
-
在 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="presenter" type="com.i3kmsteady.databindingsample.event.Presenter" /> <variable name="person" type="com.i3kmsteady.databindingsample.entity.Person" /> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.ObservableActivity"> <android.support.v7.widget.AppCompatTextView android:id="@+id/tvName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:text="@{person.firstName}" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="默认值" /> <Button android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:onClick="@{() -> presenter.updateName(person)}" android:text="@string/update_name" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/tvName" /> </android.support.constraint.ConstraintLayout> </layout>
-
当点击按钮的时候,可以看到界面上的文字发生变化。Data binding 会生成一个 BR 类,通过这个类可以引用绑定的数据资源。
@BindingAdapters 使用
Data binding 也提供了自定义逻辑的操作,通过 @BindingAdapter
注解即可实现。并且官方文档也建议,xml 中的表达式应当保持简洁,通过回调去处理复杂的逻辑。下面来看个示例:
- 首先定义一个方法,用
@BindingAdapter
注解@BindingAdapter("isGone") fun bindIsGone(view: View, age: Int) { if (age > 13) { view.visibility = View.VISIBLE } else { view.visibility = View.GONE } }
括号中的 isGone
表示的是在 xml ,可以使用的属性名称
- xml 中 使用
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="age" type="Integer"/> </data> <android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.BindingAdaptersActivity"> <android.support.v7.widget.AppCompatImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" app:isGone="@{age}"/> </android.support.constraint.ConstraintLayout> </layout>
data
元素中定义的 age
变量和类型,对应的是 bindIsGone()
的参数名称和类型。通过自定义属性,可以很方便的去处理更复杂的逻辑操作,使得 xml 中的代码变得简洁和容易维护。
网友评论