我为什么放弃在项目中使用Data Binding

Android笔记 专栏收录该内容
133 篇文章 0 订阅

我是怎么开始去使用它的

开始使用它的原因

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的争议看法及抉择,可点此阅读


欢迎关注我的公众号

浩码农

  • 8
    点赞
  • 11
    评论
  • 7
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值