本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Android中DataBinding的封装
先简单的介绍DataBinding
DataBinding 是谷歌官方发布的一个框架,是 MVVM 模式在 Android 上的一种实现,减少了布局和逻辑的耦合,数据能够单向或双向绑定到 layout 文件中,有助于防止内存泄漏,而且能自动进行空检测以避免空指针异常。
虽然现在Xml中可以写逻辑代码了,但是还是推荐不要直接在xml里面写复杂的逻辑,如果有必要的需求,我们可以用BindingAdapter 自定义属性。
话不多说,快来看看怎么用!
一. 如何使用
1.1.数据的绑定
gradle开启功能, 4.0以上和以下的有区别。现在很少有4.0以下的吧。
android {
viewBinding {
enabled = true
}
dataBinding{
enabled = true
}
}
// Android Studio 4.0
android {
buildFeatures {
dataBinding = true
viewBinding = true
}
}
开启DataBinding之后,xml会默认编译为Java对象,如果不想自己的非DB的xml被编译,可以在xml添加忽略
tools:viewBindingIgnore="true"
正常的xml中需要用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"
xmlns:binding="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<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>
</layout>
xml中data闭包是数据源,我们定义了类型之后需要在Activity/Fragment中设置,例如:
val view = CommUtils.inflate(R.layou.include_databinding_test)
//绑定DataBinding 并赋值自定义的数据
DataBindingUtil.bind<IncludeDatabindingTestBinding>(view)?.apply {
testBean = TestBindingBean("haha", "heihei", "huhu")
click = clickProxy
}
绑定数据的类型我们可以用LiveData或Flow都可以:
val etLiveData: MutableLiveData<String> = MutableLiveData()
val etFlow: MutableStateFlow<String?> = MutableStateFlow(null)
直接XML中双向绑定数据或者单向的绑定数据即可
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={click.etLiveData}" />
1.2 布局的绑定
inflate/include/ViewStub/CustomView如何绑定布局与DataBinding
include/ViewStub这样用:
<include
layout="@layout/include_databinding_test"
binding:click="@{click}"
binding:testBean="@{testBean}" />
include_databinding_test:
<?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:binding="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<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>
</layout>
include:
//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中手动定义是一样的。这里演示手动定义赋值:
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中绑定属性:
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和控件的事件监听回调:
<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封装的代码中使用高阶函数来接收回调
inner class ClickProxy {
//金额变化
val onAmountChanged: (String) -> Unit = {
calculationTotalAmount(it)
}
...
}
点击事件的封装: 注意一个是我的封装clicks,一个是远程android的属性click
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="双向绑定(获取值)"
binding:clicks="@{click.showETText}" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/d_20dp"
android:layout_weight="1"
android:text="双向绑定(赋值)"
android:onClick="@{(v)->click.setData2ET(v)}"/>
xml中原生的onClick有多种方法如下:
//默认写法
android:onClick="@{(v)->click.setData2ET(v)}"
//简写
android:onClick="@{click::setData2ET}"
//带参数
android:onClick="@{(v) -> click.onSaveClick(v, task)}"
//判断逻辑调用不同的fun
android:onClick="@{(v) -> v.isVisible() ? doSomething() : void}"
回调的方法也不同:
inner class ClickProxy {
fun showETText() {
toast(etLiveData.value)
}
fun setData2ET(view: View) {
etLiveData.value = "设置数据给ET"
}
fun testToast() {
toast("测试吐司")
}
}
1.4 自定义事件的封装-BindingAdapter
我自己封装的比较多,有些和业务比较关联,常见的一些事件封装如下:
/**
* 设置控件的隐藏与显示
*/
@BindingAdapter("isVisibleGone")
fun isVisibleGone(view: View, isVisible: Boolean) {
view.visibility = if (isVisible) View.VISIBLE else View.GONE
}
/**
* 点击事件防抖动的点击
*/
@BindingAdapter("clicks")
fun clicks(view: View, action: () -> Unit) {
view.click { action() }
}
/**
* 下划线
*/
@BindingAdapter("isUnderline")
fun setUnderline(textView: TextView, isUnderline: Boolean) {
if (isUnderline) {
textView.paint.flags = Paint.UNDERLINE_TEXT_FLAG//下划线
textView.paint.isAntiAlias = true //抗锯齿
} else {
textView.paint.flags = 0
}
}
@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
}
}
图片加载相关:使用图片加载引擎加载图片满足各种圆角,和常用属性:
/**
* 设置图片的加载
*/
@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)
}
图片的属性使用:
<ImageView
android:id="@+id/iv_img_2"
android:layout_width="@dimen/d_100dp"
android:layout_height="@dimen/d_100dp"
android:layout_marginLeft="@dimen/d_50dp"
android:scaleType="centerCrop"
binding:imgUrl="@{viewModel.img2LiveData}"
binding:placeholder="@{@drawable/home_list_plachholder}"
binding:roundRadius="@{10}" />
EditText的监听和属性封装:
/**
* 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 = arrayOf<InputFilter>(ETMoneyValueFilter(num))
}
EditText的使用:
<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}" />
还有一些和业务关联的Adapter,比如比较常见的,根据不同的type展示不同的图片:
@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是一样的用法,不复杂。
**
* 基类的BRVAH的DataBinding封装
*/
open class BaseDataBindingAdapter<T>(layoutResId: Int, br: Int, list: MutableList<T> = mutableListOf()) :
BaseQuickAdapter<T, BaseViewHolder>(layoutResId, list), LoadMoreModule {
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()
}
}
}
多布局的封装
/**
* 基类的BRVAH的DataBinding封装(多布局)
* 需要再子类实现多布局逻辑,这里只是实现了Item的DataBinding
*/
open class BaseMultiDataBindingAdapter<T : MultiItemEntity>(
br: Int,
list: MutableList<T> = mutableListOf()
) : BaseMultiItemQuickAdapter<T, BaseViewHolder>(list), LoadMoreModule {
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()
}
}
}
使用:
比如是好友列表,三行代码就能实现:
var mDatas = mutableListOf<MyFriends>()
val mAdapter by lazy { BaseDataBindingAdapter(R.layout.item_friends, BR.item, mDatas) }
这样就有可以了,当然RV还是要设置的
private fun initRV() {
mBinding.recyclerView.vertical().adapter = mViewModel.mAdapter
}
xml中绑定对应的数据就行了
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:binding="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<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,注册多布局的类型:
class MyRewardsActiveAdapter(br: Int, list: MutableList<MyRewardsListData>) : BaseMultiDataBindingAdapter<MyRewardsListData>(br, list) {
init {
// 绑定 layout 对应的 type -- 分Rewards和Voucher
addItemType(Constants.ITEM_MORE, R.layout.item_voucher_active)
addItemType(Constants.ITEM, R.layout.item_rewards_active)
}
}
使用Adapter:
private fun initRV() {
mViewModel.mAdapter = MyRewardsActiveAdapter(BR.item, mViewModel.mDatas)
mBinding.recyclerView.vertical().adapter = mViewModel.mAdapter
}
2.2 Activity/Fragment的封装
对DataBinding的绑定布局,添加数据功能进行封装:
//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的封装中:
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即可:
class MyRewardsDetailFragment(private val myRewardsId: String?) :
YYBaseVDBFragment<MyRewardsDetailViewModel, FragmentMyRewardsDetailBinding>() {
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如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:binding="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="RtlHardcoded">
<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:
class RewardsRedeemFragment(private val myRewardsId: String?) : YYBaseVDBFragment<RewardsRedeemViewModel, FragmentRewardsRedeemBinding>() {
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常用用法和封装就完成了,更多代码可以查看源码。
完结啦