以最快的速度RecyclerView:解析库


Ilya Nekrasov, Mahtalitet ,KODE的Android开发人员
在Android开发的两年半中,我设法完成了完全不同的项目:从驾驶员和拉脱维亚银行的社交网络到联邦奖金系统和第三家运输航空公司。 无论如何,在每个项目中,我遇到了一些任务,当使用RecyclerView类实现列表时,这些任务需要搜索非经典解决方案。
本文-为在DevFest Kaliningrad'18上的演出做准备以及与同事交流的成果-对初学者和仅使用现有一个库的开发人员特别有用。


首先,我们深入探讨问题的实质和痛苦的根源,即应用程序开发中功能的增长以及所使用列表的复杂性。


第一章,客户梦想着一个应用程序,而我们-关于明确的要求


让我们想象一下一种情况,当客户联系一家想要为橡皮鸭店提供移动应用程序的工作室时。


该项目发展迅速,新想法经常出现,并且没有长期规划。


首先,客户要求我们显示现有商品的清单,并在单击时填写交货请求。 您无需走远,就可以找到解决方案:我们使用RecyclerView中的经典套件,它是针对该套件和Activity的简单自行编写的适配器。


对于适配器,我们使用同类数据,一个ViewHolder和简单的绑定逻辑。


鸭适配器
class DucksClassicAdapter( private val data: List<Duck>, private val onDuckClickAction: (Duck) -> Unit ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(rubberDuckView) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val duck = data[position] holder.divider.isVisible = position != 0 holder.rubberDuckImage.apply { Picasso.get() .load(duck.icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(R.drawable.duck_stub) .into(this) } holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) } } override fun getItemCount() = data.count() class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val clicksHolder: View = view.findViewById(R.id.clicksHolder) val divider: View = view.findViewById(R.id.divider) } } 



随着时间的流逝,客户开始想到将另一类商品添加到橡皮鸭中,这意味着他将不得不添加新的数据模型和新的布局。 但最重要的是,适配器中将出现另一个ViewType ,您可以使用它确定用于特定列表项的ViewHolder。


之后,将标题添加到类别,根据类别可以折叠和扩展每个类别,以简化商店中用户的定位。 这是标题的另一个ViewTypeViewHolder 。 另外,您将不得不使适配器复杂化,因为您需要保留一个开放组列表,并使用它来检查是否需要通过单击标题来隐藏和显示此元素或那个元素。


一切皆有适配器
 class DucksClassicAdapter( private val onDuckClickAction: (Pair<Duck, Int>) -> Unit, private val onSlipperClickAction: (Duck) -> Unit, private val onAdvertClickAction: (Advert) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { var data: List<Duck> = emptyList() set(value) { field = value internalData = data.groupBy { it.javaClass.kotlin } .flatMap { groupedDucks -> val titleRes = when (groupedDucks.key) { DuckSlipper::class -> R.string.slippers RubberDuck::class -> R.string.rubber_ducks else -> R.string.mixed_ducks } groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) } } .toMutableList() duckCountsAdapters = internalData.map { duck -> val rubberDuck = (duck as? RubberDuck) DucksCountAdapter( data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count }, onCountClickAction = { onDuckClickAction.invoke(it) } ) } } private val advert = DuckMockData.adverts.orEmpty().shuffled().first() private var internalData: MutableList<Duck> = mutableListOf() private var duckCountsAdapters: List<DucksCountAdapter> = emptyList() private var collapsedHeaders: MutableSet<Duck> = hashSetOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_RUBBER_DUCK -> { val view = parent.context.inflate(R.layout.item_rubber_duck, parent) DuckViewHolder(view) } VIEW_TYPE_SLIPPER_DUCK -> { val view = parent.context.inflate(R.layout.item_duck_slipper, parent) SlipperViewHolder(view) } VIEW_TYPE_HEADER -> { val view = parent.context.inflate(R.layout.item_header, parent) HeaderViewHolder(view) } VIEW_TYPE_ADVERT -> { val view = parent.context.inflate(R.layout.item_advert, parent) AdvertViewHolder(view) } else -> throw UnsupportedOperationException("view type $viewType without ViewHolder") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is HeaderViewHolder -> bindHeaderViewHolder(holder, position) is DuckViewHolder -> bindDuckViewHolder(holder, position) is SlipperViewHolder -> bindSlipperViewHolder(holder, position) is AdvertViewHolder -> bindAdvertViewHolder(holder) } } private fun bindAdvertViewHolder(holder: AdvertViewHolder) { holder.advertImage.showIcon(advert.icon) holder.advertTagline.text = advert.tagline holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) } } private fun bindHeaderViewHolder(holder: HeaderViewHolder, position: Int) { val item = getItem(position) as FakeDuck holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) } val arrowRes = if (collapsedHeaders.contains(item)) R.drawable.ic_keyboard_arrow_up_black_24dp else R.drawable.ic_keyboard_arrow_down_black_24dp holder.arrow.setImageResource(arrowRes) holder.title.setText(item.titleRes) } private fun changeCollapseState(item: FakeDuck, position: Int) { val isCollapsed = collapsedHeaders.contains(item) if (isCollapsed) { collapsedHeaders.remove(item) } else { collapsedHeaders.add(item) } // 1 to add items after header val startPosition = position + 1 if (isCollapsed) { internalData.addAll(startPosition - ADVERTS_COUNT, item.items) notifyItemRangeInserted(startPosition, item.items.count()) } else { internalData.removeAll(item.items) notifyItemRangeRemoved(startPosition, item.items.count()) } notifyItemChanged(position) } @SuppressLint("SetTextI18n") private fun bindSlipperViewHolder(holder: SlipperViewHolder, position: Int) { val slipper = getItem(position) as DuckSlipper holder.duckSlipperImage.showIcon(slipper.icon) holder.duckSlipperSize.text = ": ${slipper.size}" holder.clicksHolder.setOnClickListener { onSlipperClickAction.invoke(slipper) } } private fun bindDuckViewHolder(holder: DuckViewHolder, position: Int) { val duck = getItem(position) as RubberDuck holder.rubberDuckImage.showIcon(duck.icon) holder.rubberDuckCounts.adapter = duckCountsAdapters[position - ADVERTS_COUNT] val context = holder.itemView.context holder.rubberDuckCounts.layoutManager = LinearLayoutManager(context, HORIZONTAL, false) } override fun getItemViewType(position: Int): Int { if (position == 0) return VIEW_TYPE_ADVERT return when (getItem(position)) { is FakeDuck -> VIEW_TYPE_HEADER is RubberDuck -> VIEW_TYPE_RUBBER_DUCK is DuckSlipper -> VIEW_TYPE_SLIPPER_DUCK else -> throw UnsupportedOperationException("unknown type for $position position") } } private fun getItem(position: Int) = internalData[position - ADVERTS_COUNT] override fun getItemCount() = internalData.count() + ADVERTS_COUNT class DuckViewHolder(view: View) : RecyclerView.ViewHolder(view) { val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage) val rubberDuckCounts: RecyclerView = view.findViewById(R.id.rubberDuckCounts) } class SlipperViewHolder(view: View) : RecyclerView.ViewHolder(view) { val duckSlipperImage: ImageView = view.findViewById(R.id.duckSlipperImage) val duckSlipperSize: TextView = view.findViewById(R.id.duckSlipperSize) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) { val title: TextView = view.findViewById(R.id.headerTitle) val arrow: ImageView = view.findViewById(R.id.headerArrow) val clicksHolder: View = view.findViewById(R.id.clicksHolder) } class AdvertViewHolder(view: View) : RecyclerView.ViewHolder(view) { val advertTagline: TextView = view.findViewById(R.id.advertTagline) val advertImage: ImageView = view.findViewById(R.id.advertImage) } } private class FakeDuck( val titleRes: Int, val items: List<Duck> ) : Duck private fun ImageView.showIcon(icon: String, placeHolderRes: Int = R.drawable.duck_stub) { Picasso.get() .load(icon) .config(Bitmap.Config.ARGB_4444) .fit() .centerCrop() .noFade() .placeholder(placeHolderRes) .into(this) } private class DucksCountAdapter( private val data: List<Pair<Duck, Int>>, private val onCountClickAction: (Pair<Duck, Int>) -> Unit ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val view = parent.context.inflate(R.layout.item_duck_count, parent) return CountViewHolder(view) } override fun getItemCount() = data.count() override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { (holder as CountViewHolder).count.apply { val item = data[position] text = item.second.toString() setOnClickListener { onCountClickAction.invoke(item) } } } class CountViewHolder(view: View) : RecyclerView.ViewHolder(view) { val count: TextView = view.findViewById(R.id.count) } } 

我认为您明白了这一点-这样的小堆就像健康的发展。 未来,来自客户的需求也越来越多:将广告横幅固定在列表的顶部,以实现选择订购鸭子的数量的可能性。 只有这些任务最终会变成常规的适配器,再次必须从头开始编写。


github历史上开发经典适配器的过程

总结


实际上,情况一点也不令人鼓舞:在特定情况下,必须对单个适配器进行锐化。 我们都知道,在实际的应用程序中,有数十个甚至数百个这样的列表屏幕。 它们不包含有关鸭子的信息,而包含更复杂的数据。 是的,它们的设计要复杂得多。


我们的适配器有什么问题?


  • 显然,它们很难重用;
  • 内部出现了业务逻辑,随着时间的流逝,它变得越来越多;
  • 难以维护和扩展;
  • 更新数据时出错的风险很高;
  • 非显而易见的设计。

第二章,一切都可能有所不同


想象未来几年应用程序的发展是不现实和毫无意义的。 在上一章用铃鼓跳舞了几次之后,写了几十个适配器,任何人都会有一个疑问:“也许还有其他解决方案吗?”。



完成Github之后,我们发现第一个AdapterDelegates库出现在2015年,一年后, GroupieEpoxy加入了开发人员的队伍 -它们都使生活变得更轻松,但是每个都有自己的特点和陷阱。


还有更多类似的库(例如FastAdapter),但是我和我的同事都没有使用它们,因此在本文中我们将不考虑它们。

在比较这些库之前,我们在使用AdapterDelegates的条件下与在线商店简要分析了上述情况-从反汇编库的角度来看,这是最简单的(从内部实现和使用的角度来看(然而,它并不是所有内容都先进的,因此您必须手动添加很多东西))。


该库不会完全将我们从适配器中拯救出来,而是由块(砖)组成,我们可以安全地将它们添加到列表中或从列表中删除并交换它们。


块适配器
 class DucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() { init { delegatesManager.addDelegate(RubberDuckDelegate()) } fun setData(items: List<DisplayableItem>) { this.items = items notifyDataSetChanged() } } private class RubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() { override fun isForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean { return item is RubberDuckItem } override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false) return ViewHolder(item) } override fun onBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) { viewHolder.apply { rubberDuckImage.showIcon(item.icon) } } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage) } } 

使用活动适配器
 class DucksDelegatesActivity : AppCompatActivity() { private lateinit var ducksList: RecyclerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_ducks_delegates) ducksList = findViewById(R.id.duckList) ducksList.apply { layoutManager = LinearLayoutManager(this@DucksDelegatesActivity) adapter = createAdapter().apply { showData() } } } fun createAdapter(): DucksDelegatesAdapter { return DucksDelegatesAdapter() } private fun DucksDelegatesAdapter.showData() { setData(getRubberDucks()) } private fun getRubberDucks(): List<DisplayableItem> { return DuckMockData.ducks.orEmpty().map { RubberDuckItem(it.icon) } } } 

在第一个任务中,我们看到了不同之处:我们有一个从库继承的适配器类。 另外-同一块砖,称为代理 ,从中我们也继承并实现所需逻辑的一部分。 接下来,我们向管理器添加一个委托-这也是一个库类。 最后,您需要创建一个适配器并向其中填充数据


为了实现商店的第二类和标题,我们将编写更多的委托,并且由于DiffUtil类而使动画出现。


在这里,我将给出一个简短但明确的结论:即使在具有在线商店的情况下使应用程序复杂化,但没有缺点的情况下,即使使用此库也可以解决所有列出的问题。


github历史中使用AdapterDelegates进行的适配器开发过程

第三章,开发人员通过比较库来删除粉红色眼镜


我们将更详细地介绍每个库的功能和操作。 一种还是另一种方式,我在项目中使用了所有三个库,具体取决于任务和应用程序的复杂性。


适配器代表


我们在最大的俄罗斯航空公司之一的应用程序中使用此库。 我们需要将简单的付款清单替换为带有组和大量不同参数的清单。


简化的库工作方案如下所示:


主要类是DelegateAdapter ,各种“砖”是“ 代理 ”,它们负责显示特定的数据类型,当然还包括列表本身。


优点:


  • 浸入的简单性;
  • 易于重用的适配器;
  • 方法和类很少;
  • 没有反射,代码生成和数据绑定。

缺点:


  • 您需要自己实现逻辑,例如 通过DiffUti更新项目 l(从版本3.1.0开始,可以使用AsyncListDifferDelegationAdapter适配器);
  • 冗余代码。

通常,该库解决了扩展应用程序功能的所有主要困难,并且适合以前没有使用过该库的用户。 但是,仅就此停止,我不建议。

追星族


我们经常使用丽莎·雷Lisa Wray)几年前创建的Groupie ,包括完全使用拉脱维亚一家银行编写应用程序。


为了使用此库,首先,您需要处理依赖项 。 除了主要选项外,您还可以使用以下几种选项进行选择:



我们只讨论一件事,并规定必要的依赖关系。


以一个带有鸭子的网上商店为例,我们需要创建一个从库类继承的Item ,指定布局并通过Kotlin语法实现绑定。 与我必须使用AdapterDelegates编写的代码量相比,这只是天堂。


剩下的就是将RecyclerView GroupieAdapter设置为适配器,并将模拟的项目放入其中。



可以看出,该工作方案更大,更复杂。 在这里,除了简单的项目外,您还可以使用整个部分-项目组和其他类。


优点:


  • 直观的界面,尽管api使您思考;
  • 盒装溶液的存在;
  • 细分为元素组;
  • 在常用选项Kotlin扩展和DataBinding之间进行选择;
  • 嵌入ItemDecoration和动画。

缺点:


  • 不完整的维基;
  • 维护者和社区的支持不佳;
  • 在第一版中必须规避的小错误;
  • 区别于主线程(目前);
  • 目前不支持AndroidX(但您需要跟踪存储库)。

非常重要的一点是, Groupie具有所有缺点,可以轻松替换AdapterDelegates ,尤其是如果您打算制作第一级折叠列表并且不想编写很多样板时。


用Groupie 实施 Duck List

环氧胶


我们最近才开始使用的最后一个库是由Airbnb家伙开发的Epoxy 。 该库很复杂,但是它允许您解决所有任务。 Airbnb程序员自己使用它直接从服务器渲染屏幕。 环氧在我们的最新项目之一中派上了用场-在叶卡捷琳堡申请银行业务。


要开发屏幕,我们必须处理不同类型的数据,大量列表。 屏幕之一真是无尽。 环氧树脂帮助我们所有人解决了这一问题。


整个库的原理与前两个相似,除了使用EpoxyController代替适配器来构建列表外,该列表使您可以声明性地确定适配器的结构。



为此,该库建立在代码生成上。 它是如何工作的-包含所有细微差别,在Wiki中已得到充分描述,并在示例中得到了反映。


优点:


  • 从通常的View生成的列表模型,可以在简单的屏幕中重用;
  • 屏幕的说明性描述;
  • DataBinding以最快的速度-直接从布局文件生成模型;
  • 不仅显示块列表,还显示复杂的屏幕;
  • 在活动上共享ViewPool;
  • 开箱即用的异步差异(AsyncEpoxyController);
  • 无需蒸蒸日上的清单。

缺点:


  • 大量的类,处理器,注释;
  • 从头开始难以潜水;
  • 使用ButterKnife插件在模块中生成R2文件;
  • 了解如何正确使用回调非常困难(我们自己还不了解);
  • 有一些问题需要避免:例如,使用相同的ID崩溃

用环氧树脂实现鸭子列表

总结


我要传达的主要内容是:不要忍受需要制作复杂列表并不断重做它们时出现的复杂性。 而且这种情况经常发生。 原则上,当实施它们时,如果项目才刚刚开始,或者您正在参与其重构。


现实情况是,没有必要再使逻辑复杂化,而认为我们自己的某种抽象就足够了。 它们足够短,并且与它们一起工作不仅是一种乐趣,而且还存在将逻辑的一部分转移到UI部分(不应该存在的部分)的诱惑。 有一些工具可以帮助您避免大多数问题,因此您需要使用它们。


我知道对于许多有经验的(不仅是)开发人员,这是显而易见的,或者他们可能与我不同意。 但是我认为再次强调这一点很重要。

那么,该选择什么呢?


建议留在一个图书馆非常困难,因为选择取决于许多因素:从个人喜好到项目的意识形态。


我将执行以下操作:


  1. 如果您只是开始开发,请尝试使用AdapterDelegates一个小项目 开始 -这是最简单的库-不需要任何特殊知识。 您将了解如何使用它以及为什么它比自己编写适配器更方便。
  2. Groupie适合那些已经对AdapterDelegates玩了足够的东西而又厌倦了编写一堆样板的人,或者适合所有其他希望立即从中间开始的人。 并且不要忘记开箱即用折叠组的存在-这也是她赞成的一个很好的论据。
  3. 好吧, Epoxy-对于那些面对真正复杂项目,拥有大量数据的人来说,胖库的复杂性将不再是问题。 起初很难,但是列表的实现似乎有点麻烦。 支持Epoxy的一个重要论点可能是项目中存在DataBindingMVVM-它是为此而创建的,因为它有可能从相应的布局生成模型。

如果仍有问题,可以查看链接以再次查看我们应用程序的代码以及详细信息。

Source: https://habr.com/ru/post/zh-CN428525/


All Articles