我為什麼放棄在項目中使用Data Binding
我是怎麼開始去使用它的
開始使用它的原因
Data Binding出現也有幾年了,一直沒有去用它的主要原因是它的寫法讓我覺得會把業務邏輯與界面過度耦合在一起。但前段時間還是試用了一下。
會想去用它一共有四個原因。
一是說到底沒有用過,感覺如果與他人討論到它難免有空談的心虛感,畢竟一項技術是好是壞還是使用過後再去評論比較有底氣。
二是想先引入Data Binding,再在項目中結合它嘗試MVVM模式,畢竟Data Binding的使用方式看起來與MVVM相當的切合。
三是高效。我們通過findViewById(id)的方式找到控制項並賦值,每次都會對視圖樹進行循環和遞歸直到找到,而Data Binding只會遍歷一次視圖樹,然後找出所有需要綁定的控制項並進行賦值,相比之下要高效很多。
四是我想既然有不少人推崇它,那麼除了它明顯的耦合的問題之外,應該有其他的優勢,並且這種優勢使得它所帶來的代碼的耦合度問題也可以接受。
基於這幾個原因,我先在自己業餘之下寫的一個小項目中去使用它。
我的封裝
我這個項目的地址為:https://github.com/msdx/gradle-doc-apk 。這是一個在手機上方便瀏覽Gradle文檔的應用程序,業務邏輯非常簡單,主要是幾個簡單的列表界面再加上一個顯示文檔章節內容的WebView,使用到Data Binding的就是裡面的列表界面了。
一個項目里的封裝,不應該脫離於項目本身的使用場景。不需要過度設計,而是要簡潔高效。
在我的這個應用里,列表的邏輯都很簡單,每個列表類型單一,每個Item里都只需要一個變數,並且除了圖片載入之外,數據綁定在xml中就可以表達。所在在調用的代碼中,我並不需要關心item布局所對應的具體的ViewDataBinding類型。因此,在這裡我的封裝也很簡單。
我主要封裝了一個ViewHolder和一個Adapter。ViewHolder里只需要持有一個ViewDataBinding成員,代碼如下:
class BindingHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
1
由於列表簡單,不需要再在綁定數據時執行其他的邏輯,並且只有一個Data Binding的變數,所以在它的構造方法中傳入item布局及BR id,然後在裡面實現onCreateViewHolder(parent: ViewGroup, viewType: Int)及onBindViewHolder(holder: BindingHolder, position: Int)方法,完整代碼如下:
class BaseListAdapter<D>(
private val layoutId: Int,
private val brId: Int
) : RecyclerView.Adapter<BindingHolder>() {
private val list = ArrayList<D>()
var onItemClickListener: OnItemClickListener<D>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, layoutId, parent, false)
val holder = BindingHolder(binding)
return holder.apply {
itemView.setOnClickListener {
val position = adapterPosition
onItemClickListener?.onItemClick(it, position, list[position])
}
}
}
override fun getItemCount() = list.size
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
holder.binding.setVariable(brId, list[position])
holder.binding.executePendingBindings()
}
fun update(list: List<D>) {
this.list.clear()
this.list.addAll(list)
notifyDataSetChanged()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
接下來使用的時候就很簡單了,不需要寫ViewHolder,不需要實現onBindViewHolder,只需要使用item布局及brId構造我們的BaseListAdapter即可,代碼可謂是簡潔到髮指。具體修改可參見:https://github.com/msdx/gradle-doc-apk/commit/b999e30cb7801bbb1cab9ad511cf461d47eed625 。
從使用到放棄
有了前面的良好經歷,於是我把Data Binding引入到了公司的項目中。
項目原來已經封裝了一個ListAdapter,它的聲明如下:
abstract class ListAdapter<VH : RecyclerView.ViewHolder, D>(list: List<D> = ArrayList()) : RecyclerView.Adapter<VH>() {
private val list: MutableList<D>
// 略...
}
1
2
3
4
5
它沒有實現onCreateViewHolder(parent: ViewGroup, viewType: Int)及fun onBindViewHolder(holder: BindingHolder, position: Int)方法,這兩個方法是交由具體界面在使用時去實現的。
簡單列表的封裝
由於在這個項目中,同樣沒有多類型的列表,所以我先繼承自這個Adapter實現了單一類型列表的封裝,如下:
class BindingHolder(val binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root)
1
class BindingListAdapter<D>(
@LayoutRes private val layoutId: Int,
private val brId: Int
) : ListAdapter<BindingHolder, D>() {
var onItemClickListener: OnItemClickListener<D>? = null
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, layoutId, parent, false)
val holder = BindingHolder(binding)
onItemClickListener?.let {
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
onItemClickListener?.invoke(position, getItem(position))
}
}
return holder
}
final override fun onBindViewHolder(holder: BindingHolder, position: Int) {
holder.binding.setVariable(brId, getItem(position))
holder.binding.executePendingBindings()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
粘性頭部列表的封裝
另外,這裡項目還有一種列表,它看起來是一種分組列表,並且組標題上拉時有粘性效果。對於這種界面需求,我使用了開源庫timehop/sticky-headers-recyclerview來實現,它需要我們的Adapter類實現它的StickyRecyclerHeadersAdapter<VH extends RecyclerView.ViewHolder>介面,所以我繼承自前面所封裝的BindingListAdapter類,實現如下:
class StickyBindListAdapter<V : HeaderItem<*>>(
@LayoutRes private val headerLayoutId: Int,
private val headerBrId: Int,
itemLayoutId: Int,
itemBrId: Int
) : BindingListAdapter<V>(itemLayoutId, itemBrId), StickyRecyclerHeadersAdapter<BindingHolder> {
override fun getHeaderId(position: Int): Long = getItem(position).getHeaderId()
override fun onBindHeaderViewHolder(holder: BindingHolder, position: Int) {
holder.binding.setVariable(headerBrId, getItem(position))
holder.binding.executePendingBindings()
}
override fun onCreateHeaderViewHolder(parent: ViewGroup): BindingHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, headerLayoutId, parent, false)
return BindingHolder(binding)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
封裝之後,只需要在item的布局裡定義好控制項與變數之間的數據綁定關係,剩下的代碼就可以變得異常簡潔。
比如原來創建一個Adapter實例,代碼是這樣的:
class RechargeViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val merchant: TextView = view.findViewById(R.id.merchant)
private val value: TextView = view.findViewById(R.id.value)
private val time: TextView = view.findViewById(R.id.time)
private val count: TextView = view.findViewById(R.id.count)
fun update(record: RechargeRecord) {
merchant.text = record.merchant
value.text = record.readableDenomination
time.text = TimeFormat.transform(record.rechargeTime, TimeFormat.yyyyMMddHHmmss, TimeFormat.yyyyMMddHHmm)
if (record.category == Category.CUSTOM) {
count.text = Constants.INVALID_DATA
} else {
count.text = R.string.format_numbers.resToString(record.count)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
return object : StickyRecyclerAdapter<RechargeRecord, HeaderViewHolder, RechargeViewHolder>() {
override fun onCreateHeaderViewHolder(parent: ViewGroup?): HeaderViewHolder {
return HeaderViewHolder(inflater.inflate(R.layout.item_recycler_record_date_header, parent, false))
}
override fun onBindHeaderHolder(holder: HeaderViewHolder, value: RechargeRecord) {
holder.setText(value.getHeaderValue())
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RechargeViewHolder {
val view = inflater.inflate(R.layout.item_recycler_coupon_recharge, parent, false)
return RechargeViewHolder(view)
}
override fun onBindItemHolder(holder: RechargeViewHolder, value: RechargeRecord, position: Int) {
holder.update(value)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
現在是:
return StickyBindListAdapter(R.layout.sticky_header_record_date, BR.headerString,
R.layout.item_recycler_coupon_recharge, BR.record)
1
2
徹底幹掉了模版代碼與ViewHolder。
存在的問題
看起來似乎很美好,但是問題來了。
如果不是這種簡單的邏輯呢?
我們可以從網上的資料輕易地知道,如果要實現載入圖片,可以定義一個靜態方法,使用BindingAdapter註解,註解的value中指定一個名稱,然後Data Binding會為我們生成一個所指定名稱的自定義屬性,並關聯到這個靜態方法中去。然後我們在布局中使用這個自定義屬義,最終會執行到我們定義的這個方法中去。如下:
@BindingAdapter(value = ["loadImage"])
@JvmStatic
fun loadImage(imageView: ImageView, url: String) {
ImageLoader.loadPicture(imageView.context, url, imageView)
}
1
2
3
4
5
我們的布局中相關代碼如下:
<ImageView
android:id="@+id/image"
android:layout"match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@android:color/black"
app:loadImage="@{info.url}"/>
1
2
3
4
5
6
7
BindingAdapter給了Data Binding擴展的能力,使得原來不能在xml中綁定的值,都可以通過這種自定義屬性來處理。這是Data Binding的強大之處。
如果這種擴展的的屬性是通用的,那這真是極大的方便。但是,有時候一些業務邏輯比較複雜,難以在xml文件中描述,這樣的話也就不得不通過這種擴展的方式來實現。舉個項目中的例子,一個文本,需要根據它的狀態來顯示對應的文字及設置對應的顏色,並且這個顏色還需要考慮到皮膚的處理,在原來的代碼中是這樣的:
if (record.status == IssueRecord.Status.HAD_ISSUED || record.status == IssueRecord.Status.HAD_USED) {
state.setTextColor(SkinResourcesUtils.getColor(R.color.theme))
} else {
state.setTextColor(R.color.text_light_grey.resToColor())
}
state.setText(record.status?.text ?: 0)
1
2
3
4
5
6
如果要在xml中實現這些邏輯,代碼寫起來將會很複雜,為了避免這種複雜性,所以就有必要使用BindingAdapter為其聲明對應的靜態方法,然後在xml中為該屬性指定綁定的值:
<TextView
android:id="@+id/state"
stylex="@style/WrapContent"
android:layout_alignBaseline="@id/time"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:textSize="13sp"
app:status="@{issueRecord.status}"
tools:text="已下發"
tools:textColor="?attr/colorPrimary"/>
1
2
3
4
5
6
7
8
9
10
這樣就帶來了兩個問題:
一是,項目中這樣的業務邏輯不會少,那麼這樣的自定義屬性及相關的靜態方法也就越來越多,它們在業務上是各自為主的,所以也不適合耦合在一起,而放在各自的包里又太散亂了,以後也不好管理。
二則,對於一個界面或一個item而言,原來在一個方法或者一個類里完成的邏輯,現在被拆分到了兩個地方,一部分簡單的在xml里,另一部分在Java/Kotlin代碼里。
這是一個很大的問題。
原來xml只負責布局,現在也要負責業務UI邏輯,邏輯與布局耦合在一起了。而xml又負責得不徹底,複雜一點的還得交由Java/Kotlin去實現,它只能負責一部分,也就是邏輯將兩邊拆,各自實現一部分,這是低內聚。這樣的話就會降低代碼的閱讀性,增加項目的維護難度。這是促使我放棄的最大原因,除此之外,還有其他幾個原因,後面談。
我之所以使用Data Binding,是我認為,這樣的技術,或者說這樣的工具,應當是能夠提高開發效率的。既然能提高開發效率,那就應該去嘗試。然而它帶來的低內聚高耦合,會使得項目代碼反而變得複雜,這種複雜不是實現難度上的複雜,而是實現上的混亂導致的複雜。而這種複雜性最終是會束縛到開發效率的,因為只有保持開發上的簡單,才能夠保持開發上的效率。 Data Binding能夠使得簡單業務的代碼變得更簡單,但是它也可能使複雜業務的代碼變得更複雜,從它的特性上看,它違背了高內聚低耦合的原則,而從它的實現上看,它也沒能繞過這個問題。
總結一下放棄的理由
最後總結一下經過這些使用後讓我放棄Data Binding的理由。
一、xml代碼耦合度增加,業務邏輯內聚性降低。 不利於項目質量的持續發展。
二、經常需要手動點擊編譯,影響開發體驗。 在布局裡新增的Data Binding變數,在Java/Kotlin中要使用的時候需要先點擊編譯等待完成,否則可能引用不到對應的BR類或該類里的變數。另外,已經刪除的變數,如果不執行清理,在BR類里也依然存在,無法如R類一樣更新及時。
三、失去了Kotlin語法糖的優勢。 Kotlin擴展函數的特點可以使得代碼儘可能的簡潔直觀易於閱讀,而在xml中目前只支持Java語法而不支持Kotlin,所以也失去了使用Kotlin作為開發語言所帶來的優勢。
---------------------
作者:貌似掉線
原文:https://blog.csdn.net/maosidiaoxian/article/details/85560206
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
※MySQL分庫分表
※C++11並發編程:多線程std:thread
TAG:程序員小新人學習 |