一看就会 Android框架DataBinding的使用与封装
theme: juejin highlight: a11y-dark
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Android中DataBinding的封装
先简单的介绍DataBinding
DataBinding 是谷歌官方发布的一个框架,是 MVVM 模式在 Android 上的一种实现,减少了布局和逻辑的耦合,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常。
虽然现在Xml中可以写逻辑代码了,但是还是推荐不要直接在xml里面写复杂的逻辑,如果有必要的需求,我们可以用BindingAdapter 自定义属性。
话不多说,快来看看怎么用!
一. 如何使用
1.1.数据的绑定
gradle开启功能, 4.0以上和以下的有区别。现在很少有4.0以下的吧。 ```js android { viewBinding { enabled = true } dataBinding{ enabled = true } }
// Android Studio 4.0
android {
buildFeatures {
dataBinding = true
viewBinding = true
}
}
开启DataBinding之后,xml会默认编译为Java对象,如果不想自己的非DB的xml被编译,可以在xml添加忽略
js
tools:viewBindingIgnore="true"
```
正常的xml中需要用layout布局包裹,模板如下:
```xml
<data>
<variable
name="testBean"
type="com.guadou.kt_demo.demo.demo12_databinding_texing.TestBindingBean" />
<variable
name="click"
type="com.guadou.kt_demo.demo.demo12_databinding_texing.Demo12Activity.ClickProxy" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<!-- 注意双向绑定的写法 -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={click.etLiveData}" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="绑定(获取值)"
binding:clicks="@{click.showETText}" />
<include
layout="@layout/include_databinding_test"
binding:click="@{click}"
binding:testBean="@{testBean}" />
<com.guadou.kt_demo.demo.demo12_databinding_texing.CustomTestView
android:layout_width="match_parent"
android:layout_height="wrap_content"
binding:clickProxy="@{click}"
binding:testBean="@{testBean}" />
</LinearLayout>
```
xml中data闭包是数据源,我们定义了类型之后需要在Activity/Fragment中设置,例如:
kotlin
val view = CommUtils.inflate(R.layou.include_databinding_test)
//绑定DataBinding 并赋值自定义的数据
DataBindingUtil.bind<IncludeDatabindingTestBinding>(view)?.apply {
testBean = TestBindingBean("haha", "heihei", "huhu")
click = clickProxy
}
绑定数据的类型我们可以用LiveData或Flow都可以:
kotlin
val etLiveData: MutableLiveData<String> = MutableLiveData()
val etFlow: MutableStateFlow<String?> = MutableStateFlow(null)
直接XML中双向绑定数据或者单向的绑定数据即可
```xml
```
1.2 布局的绑定
inflate/include/ViewStub/CustomView如何绑定布局与DataBinding
include/ViewStub这样用:
xml
<include
layout="@layout/include_databinding_test"
binding:click="@{click}"
binding:testBean="@{testBean}" />
include_databinding_test:
```xml
<data>
<variable
name="testBean"
type="com.guadou.kt_demo.demo.demo12_databinding_texing.TestBindingBean" />
<variable
name="click"
type="com.guadou.kt_demo.demo.demo12_databinding_texing.Demo12Activity.ClickProxy" />
<import
alias="textUtlis"
type="android.text.TextUtils" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_marginTop="15dp"
android:text="下面是赋值的数据"
binding:clicks="@{click.testToast}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{testBean.text1}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{testBean.text2}" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{testBean.text3}" />
</LinearLayout>
include:
kotlin
//Activity中动态的加载布局
fun inflateXml() {
//给静态的xml,赋值数据,赋值完成之后 include的布局也可以自动显示
mBinding.testBean = TestBindingBean("haha", "heihei", "huhu")
//获取View
val view = CommUtils.inflate(R.layout.include_databinding_test)
//绑定DataBinding 并赋值自定义的数据
DataBindingUtil.bind<IncludeDatabindingTestBinding>(view)?.apply {
testBean = TestBindingBean("haha1", "heihei1", "huhu1")
click = clickProxy
}
//添加布局
mBinding.flContent.apply {
removeAllViews()
addView(view)
}
}
自定义View,在Xml中定义和在Activity中手动定义是一样的。这里演示手动定义赋值:
kotlin
fun customView() {
//给静态的xml,赋值数据,赋值完成之后 include的布局也可以自动显示
mBinding.testBean = TestBindingBean("haha2", "heihei2", "huhu2")
//动态的添加自定义View
val customTestView = CustomTestView(mActivity)
customTestView.setClickProxy(clickProxy)
customTestView.setTestBean(TestBindingBean("haha3", "heihei3", "huhu3"))
mBinding.flContent2.apply {
removeAllViews()
addView(customTestView)
}
}
``` 自定义Viewr中绑定属性:
```xml class CustomTestView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : LinearLayout(context, attrs, defStyleAttr) {
init {
orientation = VERTICAL
//传统的方式添加
val view = CommUtils.inflate(R.layout.layout_custom_databinding_test)
addView(view)
}
//设置属性
fun setTestBean(bean: TestBindingBean?) {
bean?.let {
findViewById<TextView>(R.id.tv_custom_test1).text = it.text1
findViewById<TextView>(R.id.tv_custom_test2).text = it.text2
findViewById<TextView>(R.id.tv_custom_test3).text = it.text3
}
}
fun setClickProxy(click: Demo12Activity.ClickProxy?) {
findViewById<TextView>(R.id.tv_custom_test1).click {
click?.testToast()
}
}
}
```
1.3 事件的绑定
比较常见的是Click和控件的事件监听回调:
xml
<EditText
android:id="@+id/et_redpack_money"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/d_9dp"
android:layout_weight="1"
android:background="@null"
android:hint="0.00"
android:inputType="numberDecimal"
android:paddingLeft="@dimen/d_9dp"
android:singleLine="true"
android:textColor="@color/black_33"
android:textCursorDrawable="@null"
android:textSize="@dimen/d_25sp"
binding:typefaceSemiBold="@{true}"
binding:onTextChanged="@{click.onAmountChanged}"
binding:setDecimalPoints="@{2}" />
Click封装的代码中使用高阶函数来接收回调
```kotlin
inner class ClickProxy {
//金额变化
val onAmountChanged: (String) -> Unit = {
calculationTotalAmount(it)
}
...
}
**点击事件的封装:**
注意一个是我的封装clicks,一个是远程android的属性click
xml
@BindingAdapter("isCenterLine") fun isCenterLine(textView: TextView, isUnderline: Boolean) { if (isUnderline) { textView.paint.flags = Paint.STRIKE_THRU_TEXT_FLAG textView.paint.isAntiAlias = true //抗锯齿 } else { textView.paint.flags = 0 } }
@BindingAdapter("setRightDrawable") fun setRightDrawable(textView: TextView, drawable: Drawable?) { if (drawable == null) { textView.setCompoundDrawables(null, null, null, null) } else { drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) textView.setCompoundDrawables(null, null, drawable, null) } }
@BindingAdapter("setLeftDrawable") fun setLeftDrawable(textView: TextView, drawable: Drawable?) { if (drawable == null) { textView.setCompoundDrawables(null, null, null, null) } else { drawable.setBounds(0, 0, drawable.minimumWidth, drawable.minimumHeight) textView.setCompoundDrawables(drawable, null, null, null) } }
@BindingAdapter("text", "default", requireAll = false) fun setText(view: TextView, text: CharSequence?, default: String?) { if (text == null || text.trim() == "" || text.contains("null")) { view.text = default } else { view.text = text } } ```
图片加载相关:使用图片加载引擎加载图片满足各种圆角,和常用属性: ```kotlin /* * 设置图片的加载 / @BindingAdapter("imgUrl", "placeholder", "roundRadius", "isCircle", requireAll = false) fun loadImg( view: ImageView, url: Any?, placeholder: Drawable? = null, roundRadius: Int = 0, isCircle: Boolean = false ) { url?.let { view.extLoad( it, placeholder = placeholder, roundRadius = CommUtils.dip2px(roundRadius), isCircle = isCircle ) } }
@BindingAdapter("loadBitmap")
fun loadBitmap(view: ImageView, bitmap: Bitmap?) {
view.setImageBitmap(bitmap)
}
图片的属性使用:
xml
EditText的监听和属性封装: ```kotlin /* * EditText的简单监听事件 / @BindingAdapter("onTextChanged") fun EditText.onTextChanged(action: (String) -> Unit) { addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
action(s.toString())
}
})
}
var _viewClickFlag = false var _clickRunnable = Runnable { _viewClickFlag = false }
/* * Edit的确认按键事件 / @BindingAdapter("onKeyEnter") fun EditText.onKeyEnter(action: () -> Unit) { setOnKeyListener { _, keyCode, _ -> if (keyCode == KeyEvent.KEYCODE_ENTER) { KeyboardUtils.closeSoftKeyboard(this)
if (!_viewClickFlag) {
_viewClickFlag = true
action()
}
removeCallbacks(_clickRunnable)
postDelayed(_clickRunnable, 1000)
}
return@setOnKeyListener false
}
}
/* * Edit的失去焦点监听 / @BindingAdapter("onFocusLose") fun EditText.onFocusLose(action: (textView: TextView) -> Unit) { setOnFocusChangeListener { _, hasFocus -> if (!hasFocus) { action(this) } } }
/* * Edit的得到焦点监听 / @BindingAdapter("onFocusGet") fun EditText.onFocusGet(action: (textView: TextView) -> Unit) { setOnFocusChangeListener { _, hasFocus -> if (hasFocus) { action(this) } } }
/*
* 设置ET智能小数点2位
/
@BindingAdapter("setDecimalPoints")
fun setDecimalPoints(editText: EditText, num: Int) {
editText.filters = arrayOfEditText的使用:
xml
还有一些和业务关联的Adapter,比如比较常见的,根据不同的type展示不同的图片:
kotlin
@BindingAdapter("setShareIcon")
fun setShareIcon(imageView: ImageView, firendGroup: MyFirendGroups) {
imageView.setImageResource(if (firendGroup.type ==0) R.drawable.dialog_share_group_icon else R.drawable.dialog_share_more_icon)
} ```
已经说了讲下简单的使用,还是写了这么多。还有很多地方没写到,不过常用的都在上面了。 下面讲下如何封装DataBinding
二. DataBinding的封装
封装之后在Activity/Fragment/RecyclerView等常用的场景更加方便的使用。
2.1 基于Adapter的封装,方便在RV中使用
这里用的是BRVAH,如果是自己封装的Adapter是一样的用法,不复杂。
```kotlin
*
* 基类的BRVAH的DataBinding封装
/
open class BaseDataBindingAdapter
private val _br: Int = br
override fun onItemViewHolderCreated(viewHolder: BaseViewHolder, viewType: Int) {
// 绑定databinding
DataBindingUtil.bind<ViewDataBinding>(viewHolder.itemView)
}
override fun convert(holder: BaseViewHolder, item: T, payloads: List<Any>) {
super.convert(holder, item, payloads)
}
override fun convert(holder: BaseViewHolder, item: T) {
if (item == null) {
return
}
holder.getBinding<ViewDataBinding>()?.run {
if (_br > 0) {
setVariable(_br, item)
}
executePendingBindings()
}
}
}
多布局的封装
kotlin
/*
* 基类的BRVAH的DataBinding封装(多布局)
* 需要再子类实现多布局逻辑,这里只是实现了Item的DataBinding
/
open class BaseMultiDataBindingAdapter
val _br: Int = br
override fun onItemViewHolderCreated(viewHolder: BaseViewHolder, viewType: Int) {
// 绑定databinding
DataBindingUtil.bind<ViewDataBinding>(viewHolder.itemView)
}
@Suppress("SENSELESS_COMPARISON")
override fun convert(holder: BaseViewHolder, item: T) {
if (item == null) {
return
}
holder.getBinding<ViewDataBinding>()?.run {
setVariable(_br, item)
executePendingBindings()
}
}
}
使用:<br> 比如是好友列表,三行代码就能实现:
kotlin
var mDatas = mutableListOf这样就有可以了,当然RV还是要设置的
kotlin
private fun initRV() {
mBinding.recyclerView.vertical().adapter = mViewModel.mAdapter
}
xml中绑定对应的数据就行了
xml
<data>
<variable
name="item"
type="com.guadou.cs_cptservices.commbean.MyFriends" />
</data>
<LinearLayout>
//xxx省略
<com.guadou.lib_baselib.view.CircleImageView
android:layout_width="45dp"
android:layout_height="45dp"
binding:imgUrl="@{item.friend_avatar}"
binding:placeholder="@{@drawable/yypay_default_head}"
android:scaleType="centerCrop"
android:src="@drawable/yypay_default_head" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:text="@{item.friend_name}"
android:textColor="@color/black"
android:textSize="16sp"
tools:text="User Name" />
</LinearLayout>
``` 这里大家应该都会,就不贴太多没用的代码,效果图如下:
RV的多布局则要写一个Adapter,注册多布局的类型:
```kotlin
class MyRewardsActiveAdapter(br: Int, list: MutableList
init {
// 绑定 layout 对应的 type -- 分Rewards和Voucher
addItemType(Constants.ITEM_MORE, R.layout.item_voucher_active)
addItemType(Constants.ITEM, R.layout.item_rewards_active)
}
}
使用Adapter:
kotlin
private fun initRV() {
mViewModel.mAdapter = MyRewardsActiveAdapter(BR.item, mViewModel.mDatas)
mBinding.recyclerView.vertical().adapter = mViewModel.mAdapter
}
```
2.2 Activity/Fragment的封装
对DataBinding的绑定布局,添加数据功能进行封装: ```kotlin //DataBinding的封装数据 class DataBindingConfig(private val layout: Int, private val vmVariableId: Int, private val viewModel: BaseViewModel?) {
constructor(layout: Int) : this(layout, 0, null)
private var bindingParams: SparseArray<Any> = SparseArray()
fun getLayout(): Int = layout
fun getVmVariableId(): Int = vmVariableId
fun getViewModel(): BaseViewModel? = viewModel
fun getBindingParams(): SparseArray<Any> = bindingParams
fun addBindingParams(variableId: Int, objezt: Any): DataBindingConfig {
if (bindingParams.get(variableId) == null) {
bindingParams.put(variableId, objezt)
}
return this
}
}
BaseActivity的封装中:
kotlin
override fun setContentView() {
mViewModel = createViewModel()
val config = getDataBindingConfig()
mBinding = DataBindingUtil.setContentView(this, config.getLayout())
mBinding.lifecycleOwner = this
if (config.getVmVariableId() != 0) {
mBinding.setVariable(
config.getVmVariableId(),
config.getViewModel()
)
}
val bindingParams = config.getBindingParams()
bindingParams.forEach { key, value ->
mBinding.setVariable(key, value)
}
}
abstract fun getDataBindingConfig(): DataBindingConfig
使用的时候我们封装填充自己的DataBindingConfig即可:
kotlin
class MyRewardsDetailFragment(private val myRewardsId: String?) :
YYBaseVDBFragment
override fun getDataBindingConfig(): DataBindingConfig {
return DataBindingConfig(R.layout.fragment_my_rewards_detail, BR.viewModel, mViewModel)
.addBindingParams(BR.click, ClickProxy()) //可以自己添加数据源
.addBindingParams(BR.item,ItmBean())
}
override fun init(savedInstanceState: Bundle?) {
initData()
}
private fun initData() {
mViewModel.getMyRewardsDetail(myRewardsId)
}
/**
* DataBinding事件处理
*/
inner class ClickProxy {
fun gotoRedeemPage() {
navigator.push({ applySlideInOut() }) {
RewardsRedeemFragment(myRewardsId)
}
}
}
}
xml如下:
xml
<data>
<variable
name="viewModel"
type="com.guadou.cpt_rewards.mvvm.vm.MyRewardsDetailViewModel" />
<variable
name="click"
type="com.guadou.cpt_rewards.ui.fragment.my.MyRewardsDetailFragment.ClickProxy" />
<variable
name="item"
type="com.guadou.cpt_rewards.entity.ItemBean" />
<import type="android.text.TextUtils" />
<import type="com.guadou.lib_baselib.utils.DateAndTimeUtil" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
xxx
</LinearLayout>
```
当然你可以不在DataBindingConfig中绑定数据和ViewModel:
```kotlin
class RewardsRedeemFragment(private val myRewardsId: String?) : YYBaseVDBFragment
override fun getDataBindingConfig(): DataBindingConfig {
return DataBindingConfig(R.layout.fragment_rewards_redeem)
}
@ExperimentalCoroutinesApi
override fun init(savedInstanceState: Bundle?) {
//获取支付码展示
mViewModel.getRedeemDetail(myRewardsId)
}
override fun startObserve() {
mViewModel.mRedeemCodeLiveData.observe(this) {
it?.let { popupData(it) }
}
}
private fun popupData(code: MyRewardsCode) {
//没有绑定ViewModel和Click对象,无法直接在xml绑定数据,那么直接拿到控件setText也是可以的
mBinding.ivQrCode.extLoad(code.url)
mBinding.tvVertyCode.text = code.code
}
private fun gotoRedeemSuccess(scanResult: RewardsScanResult) {
navigator.push({ applySlideInOut() }) {
RewardsRedeemSuccessFragment(scanResult)
}
}
} ```
到此DataBinding常用用法和封装就完成了,更多代码可以查看源码。
完结啦
- Android操作文件也太难了趴,File vs DocumentFile 以及 DocumentsProvider vs FileProvider 的异同
- findViewById不香吗?为什么要把简单的问题复杂化?为什么要用DataBinding?
- Android自定义View绘制进阶-水波浪温度刻度表
- Android自定义ViewGroup布局进阶,完整的九宫格实现
- 记录仿抖音的视频播放并缓存预加载视频的效果实现
- Kotlin对象的懒加载方式?by lazy 与 lateinit 的异同
- 定位都得集成第三方?Android原生定位服务LocationManager不行吗?
- 还用第三方库管理状态栏吗?Android关于状态栏管理的几种方案实现!
- 下载需要集成第三方?Android原生下载服务DownloadManager不行吗?
- Android阴影实现的几种方案-自定义圆角ViewGroup加入阴影效果
- 操作Android窗口的几种方式?WindowInsets与其兼容库的使用与踩坑
- Android软键盘与布局的协调-不同的效果与实现方案的探讨
- ViewPager2:ViewPager都能自动嵌套滚动了,我不行?我麻了!该怎么做?
- Android软键盘的监听与高度控制的几种方案及常用效果
- 圆角升级啦,来手把手一起实现自定义ViewGroup的各种圆角与背景
- Android导航栏的处理-HostStatusLayout加入底部的导航栏适配
- 一次搞懂怎么设置圆角图片,ImageView的各种圆角设置
- 一看就会 Android框架DataBinding的使用与封装
- 别滥用FileProvider了,Android中FileProvider的各种场景应用
- Android登录拦截的场景-基于拦截器模式实现