一看就會 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登錄攔截的場景-基於攔截器模式實現