- 1. 说明
- 2. 使用
- 2.1 环境准备
- 2.2 根据name长度显示Message案例
- 2.3 响应点击事件
- 2.4 可观察数据类型
- 2.4.1 可观测对象
- 2.4.2 可观测字段
- 2.4.3 可观察集合
- 2.5 设置数据可观察
- 2.6 数据双向绑定
- 2.6.1 方式一:继承自BaseObservable
- 2.6.2 方式二:继承自ObservableField
- 2.7 RecyclerView+dataBinding
- 3. 自定义BindingAdapter
本篇博客参考Data Binding in Android (google.cn) 和 数据绑定库
数据绑定可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。其实有点类似于MVVM
框架,数据和显示的部分动态绑定,当数据发生改变对应的视图也随之改变。如果您使用数据绑定的主要目的是取代findViewById()
调用,请考虑改用视图绑定。其模式示意图:
和视图绑定类似,对于Android Studio
的版本也有要求:
2. 使用 2.1 环境准备Android Studio 3.4 or greater
类似的,直接在配置文件中添加:
android {
...
dataBinding {
enabled = true
}
}
将之前的layout
布局文件修改为DataBinding layout
。直接右击根布局的标签元素,然后选择Show Context Actions:
然后就可以看见提供了直接转换到data binding
布局的选项:
比如我这里转换后的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>
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="内容"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.194" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
在data
标签中的内容,也就是定义的变量。比如可以定义如下的两个变量:
<data>
<import type="android.view.View"/>
<variable name="name" type="String"/>
<variable name="message" type="String"/>
data>
因为后续需要使用View
,所以这里需要导入包。对应的,可以使用自定义的类,然后导入对应的包即可。
将定义的变量和布局文件中的控件关联,也就是使用变量。在Android Jetpack
中定义的使用方式为@{ expression }
的格式,也就是可以如下使用:
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ message }"
android:visibility="@{ (name.length > 3) ? View.VISIBLE : View.GONE }"
...
/>
然后就是在代码中设置在xml
中申明的两个变量的值。和viewbinding
类似在databinding
中也需要在onCreate
方法替换:
setContentView(R.layout.plain_activity)
这里替换为:
binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this,
R.layout.activity_main
)
所获得的binding
对象也就是和布局文件相关联的类,即:ActivityMainBinding
。通过binding
这个实例,就能够直接 *** 作在xml
中声明的变量:
binding.name = "testDemo"
binding.message = "Hello data binding."
完整代码:
class MainActivity2 : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. setContentView(R.layout.plain_activity) replace with below:
// data binding
binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this,
R.layout.activity_main
)
// 2. set the variable values
binding.name = "testDemo"
binding.message = "Hello data binding."
}
}
结果:
通常我们可以直接在xml
中直接设置点击函数,比如:
android:onClick="onButtonClick"
然后在Activity
中定义方法onButtonClick
。或者直接通过这个按钮的实例对象来注册监听,进行事件处理。这里也是类似,可以在xml
中采用Lambda
表达式的方式来注册函数。首先定义一个SimpleViewModel
类,继承自ViewModel
类,如下:
/**
* @author 梦否 on 2022/3/29
* @blog https://mengfou.blog.csdn.net/
*/
class SimpleViewModel: ViewModel() {
// 定义消息
var message = "Hello data binding."
get() {
return if(clickNumber % 2 == 0) "偶数" else "奇数"
}
private set // 阻止外部修改,只支持内部修改
val name = "testDemo"
var clickNumber = 0
private set
// 定义点击函数
fun onTextViewClick(){
clickNumber++
Log.e("TAG", "onTextViewClick: ${clickNumber}" )
}
}
但是,很不幸,点击TextView
之后,在TextView
中显示的文本并没有观测到数据的变化。观察日志:
其实,这是因为我们设置的数据并不可观测。我们需要让数据可以observable才行。为了让字段可观测,可以使用observable
类或者LiveData
。关于可观察的数据对象在Google
中有详细说明:使用可观察的数据对象。
可观察类有三种不同类型:对象、字段和集合。
2.4.1 可观测对象实现Observable
接口的类允许注册监听器,以便它们接收有关可观察对象的属性更改的通知。为便于开发,数据绑定库提供了用于实现监听器注册机制的BaseObservable
类。实现BaseObservable
的数据类负责在属性更改时发出通知。具体 *** 作过程是向 getter 分配Bindable
注释,然后在 setter 中调用notifyPropertyChanged()
方法。
ObservableField()
以及基本的:
ObservableArrayMap
、ObservableArrayList
等。
因为这里所使用的为基本类型,比如message
和name
。由于这里我只需要message
可观测,所以这里对其应用可观测字段即可。如下:
/**
* @author 梦否 on 2022/3/29
* @blog https://mengfou.blog.csdn.net/
*/
class SimpleViewModel : ViewModel() {
// 定义可观测的字段,使用ObservableField
var message = ObservableField<String>("Hello data binding.")
private set // 阻止外部修改,只支持内部修改
val name = "testDemo"
var clickNumber = 0
private set
// 定义点击函数
fun onTextViewClick() {
clickNumber++
if (clickNumber.rem(2) == 0) message.set("is Even")
else message.set("is odd")
Log.e("TAG", "onTextViewClick: ${clickNumber}")
}
}
对应的修改在xml
中的标签中的变量声明:
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="com.weizu.jetpackdemo.SimpleViewModel" />
data>
对应的MainActivity
文件:
class MainActivity2 : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. setContentView(R.layout.plain_activity) replace with below:
// data binding
binding = DataBindingUtil.setContentView<ActivityMainBinding>(
this,
R.layout.activity_main
)
// set data
binding.viewModel = SimpleViewModel()
}
}
然后就可以看见点击后奇数点击和偶数点击的切换显示文本效果。
2.6 数据双向绑定在这个案例中需要达到的效果为:对应定义的可观测字段Field
内容的改变可以通知到对应的控件,而控件的内容变化也可以通知到Field
。所以这里可以使用控件EditText
。
在布局文件中定义一个EditText
和TextView
,如下:
<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="myViewModel"
type="com.weizu.jetpackdemo.databinding.MyViewModel" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName"
android:text="@={ myViewModel.userInput }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ myViewModel.userInput }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.675" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
值得注意的是,在EditText
中设置为:
android:text="@={ myViewModel.userInput }"
而在TextView
中为:
android:text="@{ myViewModel.userInput }"
因为在EditText
中我们需要完成双向绑定,即用户输入可以通知到LiveData
,而在TextView
中只要加载变化后的数据即可。
那么,在自定义ViewModel
中为:
/**
* @author 梦否 on 2022/4/20
* @blog https://mengfou.blog.csdn.net/
*/
class MyViewModel :BaseObservable(){
// 设置为LiveData,便于布局文件中TextView内容的自动更新
private val userInput = MutableLiveData<String>("Tom")
// 这里一定不要忘记添加注解@Bindable,否则双向绑定不会生效
@Bindable
@JvmName("getUserInput")
fun getUserInput(): String{
return userInput.value.toString()
}
// 用于更新TextView
fun get(): LiveData<String> {
return this.userInput
}
@JvmName("setUserInput")
fun setUserInput(str: String){
if(!str.equals(userInput)) {
this.userInput.value = str
}
Log.e("TAG", "setValue: ${str}" )
// 通知数据发生了改变
notifyPropertyChanged(BR.myViewModel) // build后会自动生成一个BR类,对应在xml中声明的变量
}
}
这里为了完成双向绑定,继承自BaseObservable
,且在get
方法上使用了@Bindable
注解来表示绑定。至于get()
方法仅是为了返回LiveData
对象,然后在Activity
中设置观察,更新TextView
控件内容:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMain2Binding>(
this,
R.layout.activity_main2
)
// 这里直接使用new一个对象
// 因为这里的自定义ViewModel继承的是BaseObservable类,不是ViewModel类
val myViewModel = MyViewModel()
binding.myViewModel = myViewModel
// 设置观察,以更新TextView文本
myViewModel.get().observe(this) {
binding.textView2.text = myViewModel.get().value
}
}
}
效果:
布局文件还是保持不变:
<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="myViewModel"
type="com.weizu.jetpackdemo.databinding.MyViewModel" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".databinding.MainActivity">
<EditText
android:id="@+id/editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPersonName"
android:text="@={ myViewModel.userInput }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ myViewModel.userInput }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.675" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
对于ViewModel
进行删减:
class MyViewModel {
// 设置为可观察类型
val userInput = ObservableField<String>("Tom")
}
最后在Activity
中进行设置数据:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMain2Binding>(
this,
R.layout.activity_main2
)
// 这里直接使用new一个对象
// 因为这里的自定义ViewModel继承的是BaseObservable类,不是ViewModel类
val myViewModel = MyViewModel()
binding.myViewModel = myViewModel
// 在xml文件:@= *** 作符进行双向绑定
}
}
达到的效果和上小节一样。
2.7 RecyclerView+dataBinding可以使用databinding
来设置每个item
的内容。比如在主布局文件:
<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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".recycleview.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
因为使用了RecyclerView
,所以这里还是定义对应的适配器:
/**
* @author 梦否 on 2022/4/20
* @blog https://mengfou.blog.csdn.net/
*/
class MyRecycleViewAdapter(var context: Context, var datas: List<User>) :
RecyclerView.Adapter<MyRecycleViewAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(context)
val binding = DataBindingUtil.inflate<RecyclerviewItemBinding>(
inflater,
R.layout.recyclerview_item,
parent,
false
)
val myViewHolder = MyViewHolder(binding.root)
myViewHolder.binding = binding
return myViewHolder
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.binding?.user = datas[position]
Log.e("TAG", "onBindViewHolder: ${position} + ${ datas[position].name }")
}
override fun getItemCount(): Int {
return datas.size
}
inner class MyViewHolder(var root: View) : RecyclerView.ViewHolder(root) {
var binding: RecyclerviewItemBinding? = null
}
}
同样的在R.layout.recyclerview_item
布局文件中设置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>
<variable
name="user"
type="com.weizu.jetpackdemo.recycleview.User" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ user.name }"
app:layout_constraintBottom_toTopOf="@+id/textView4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="93dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="20dp" />
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{ String.valueOf(user.age) }"
app:layout_constraintBottom_toTopOf="@+id/guideline3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="1dp"
android:imageSrc="@{ user.image }"
app:layout_constraintEnd_toStartOf="@+id/textView4"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
对于User
类比较简单:
class User(var age: Int, var name: String) {
var image = "https://i1.hdslb.com/bfs/face/7e72c58637ff26df68fb30939de078d2bbbfcdbe.jpg"
}
在主Activity
中配置:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding =
DataBindingUtil.setContentView<ActivityMain3Binding>(
this,
R.layout.activity_main3
)
val datas = listOf<User>(
User(12, "Jack"),
User(10, "Tom"),
User(23, "Joe")
)
// 必须设置布局管理器,否则不会显示RecyclerView
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = MyRecycleViewAdapter(this, datas)
}
}
运行即可看见效果。
3. 自定义BindingAdapter参考视频地址:https://www.bilibili.com/video/BV1Ry4y1t7Tj?p=12
这个案例感觉比较典型,达到的效果为可以使用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>
<variable
name="src"
type="String" />
data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BindingAdapterActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="300dp"
android:layout_height="300dp"
app:imageSrc="@{ src }"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
androidx.constraintlayout.widget.ConstraintLayout>
layout>
注意到,在ImageView
标签中直接设置了自定义的字段:
app:imageSrc="@{ src }"
而这个字段以前我们是需要使用tool:
并定义对应的styleable
样式。这里并不需要,仅需要使用注解来申明:
class ImageViewCus {
// 需要注意的是,这里需要使用静态方法
companion object{
@JvmStatic
@BindingAdapter("app:imageSrc")
fun loadImage(imageView: ImageView, str: String){
Glide.with(imageView.context)
.load(str)
.placeholder(R.drawable.ic_launcher_background)
.into(imageView)
}
}
}
最后是在Activity
中使用:
class MyBindingAdapterActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityBindingAdapterBinding>(
this,
R.layout.activity_binding_adapter
)
binding.src = "http://www.kaotop.com/file/tupian/20220426/5690c131d90e460fa4c96bf86b1ae634.png"
}
}
传入databinding
中声明的字符串即可,就可以达到预期的效果。整体的使用流程感觉和SpringBoot
中的类似,但是这里比较好奇的是难道这里也会扫描所有包/类中的注解?应该是的,等储备知识够了再深入。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)