我为什么放弃在项目中使用Data Binding
Posted 貌似掉线
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我为什么放弃在项目中使用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)
由于列表简单,不需要再在绑定数据时执行其他的逻辑,并且只有一个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()
接下来使用的时候就很简单了,不需要写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>
// 略...
它没有实现onCreateViewHolder(parent: ViewGroup, viewType: Int)
及fun onBindViewHolder(holder: BindingHolder, position: Int)
方法,这两个方法是交由具体界面在使用时去实现的。
简单列表的封装
由于在这个项目中,同样没有多类型的列表,所以我先继承自这个Adapter实现了单一类型列表的封装,如下:
class BindingHolder(val binding: ViewDataBinding): RecyclerView.ViewHolder(binding.root)
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()
粘性头部列表的封装
另外,这里项目还有一种列表,它看起来是一种分组列表,并且组标题上拉时有粘性效果。对于这种界面需求,我使用了开源库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)
封装之后,只需要在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)
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)
现在是:
return StickyBindListAdapter(R.layout.sticky_header_record_date, BR.headerString,
R.layout.item_recycler_coupon_recharge, BR.record)
彻底干掉了模版代码与ViewHolder。
存在的问题
看起来似乎很美好,但是问题来了。
如果不是这种简单的逻辑呢?
我们可以从网上的资料轻易地知道,如果要实现加载图片,可以定义一个静态方法,使用BindingAdapter
注解,注解的value
中指定一个名称,然后Data Binding会为我们生成一个所指定名称的自定义属性,并关联到这个静态方法中去。然后我们在布局中使用这个自定义属义,最终会执行到我们定义的这个方法中去。如下:
@BindingAdapter(value = ["loadImage"])
@JvmStatic
fun loadImage(imageView: ImageView, url: String)
ImageLoader.loadPicture(imageView.context, url, imageView)
我们的布局中相关代码如下:
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:background="@android:color/black"
app:loadImage="@info.url"/>
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)
如果要在xml中实现这些逻辑,代码写起来将会很复杂,为了避免这种复杂性,所以就有必要使用BindingAdapter
为其声明对应的静态方法,然后在xml中为该属性指定绑定的值:
<TextView
android:id="@+id/state"
style="@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"/>
这样就带来了两个问题:
一是,项目中这样的业务逻辑不会少,那么这样的自定义属性及相关的静态方法也就越来越多,它们在业务上是各自为主的,所以也不适合耦合在一起,而放在各自的包里又太散乱了,以后也不好管理。
二则,对于一个界面或一个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作为开发语言所带来的优势。
关于DataBinding,见仁见智。评论中有一篇文章我觉得写得非常好,深入阐述了双向绑定的场景及使用,以及作者对DataBinding的争议看法及抉择,可点此阅读。
欢迎关注我的公众号
以上是关于我为什么放弃在项目中使用Data Binding的主要内容,如果未能解决你的问题,请参考以下文章
从零開始的Android新项目7 - Data Binding入门篇
Android开发教程 - 使用Data Binding 介绍
Data Binding Guide——google官方文档翻译(上)