RecyclerView 2020:使用DataBinding在Android中处理列表的现代方法—第2部分

In the first article of this series, we discussed some disadvantages of the traditional “1 adapter per case” approach of working with RecyclerView in Android. I also demonstrated a simplified concept of a universal, DataBinding-based RecyclerView.Adapter which could serve a potential solution to most of the discussed issues. In this post, I’d like to dive a little bit deeper into this topic and try to improve the original adapter by applying it to some real-world examples.

在本系列的第一篇文章中,我们讨论了在Android中使用RecyclerView的传统“ RecyclerView 1个适配器”方法的一些缺点。 我还演示了通用的,基于DataBinding的RecyclerView.Adapter的简化概念,它可以为大多数讨论的问题提供潜在的解决方案。 在本文中,我想更深入地探讨该主题,并通过将其应用于一些实际示例中来尝试改进原始适配器。

I also received a couple of interesting questions and I’m going to discuss the most popular ones as well. Thanks for your feedback for the Part 1 and welcome to the Part 2 :-)

我还收到了一些有趣的问题,并且我还将讨论最受欢迎的问题。 感谢您对第1部分的反馈,并欢迎进入第2部分:-)

Important: Please make sure you’ve already checked out Part 1 as I’m going to continue from where we stopped there. And here’s the library I created If you just want to play with this universal adapter in practice with minimal extra reading.

重要提示 :请确保您已经检查了第1部分,因为我将从我们在那里停下来的地方继续。 这是我创建的库,如果您只是想在实践中使用此通用适配器,而只需很少的额外阅读。

Let’s take our adapter from the previous post and try to apply it to a more dynamic case: this time we want to reload our screen every time it is resumed or reopened:

我们从上一篇文章中获取我们的适配器 ,并尝试将其应用于更具动态性的案例:这次,我们希望在每次恢复或重新打开屏幕时都重新加载屏幕:

class MyViewModel : ViewModel() {    val data = MutableLiveData<List<RecyclerItem>()    // Called from Fragment's onStart()
fun loadData() {
SomeService.getUsers { users ->
data.value =
users.map { UserItemViewModel(it) }
.map { it.toRecyclerItem() }
}
}
}private fun UserItemViewModel.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.viewModel,
layoutId = R.layout.item_user)data class User(
val id: Int,
val firstName,
val lastname
)class UserItemViewModel(val user: User) {
// Some logic
}

So we just update our LiveData every time the data is loaded and through the LiveData -> Fragment -> DataBinding -> BindingAdapter chain it will be delivered to our adapter’s updateData(list: List<RecyclerItem>) method. So far so good, but let’s check how this update operation looks like visually for the case when we already had some data in the RecyclerView and our service returned the same result as it did the previous time:

因此,每次加载数据时,我们只需更新LiveData并通过LiveData-> Fragment-> DataBinding-> BindingAdapter链将其传递到适配器的updateData(list: List<RecyclerItem>)方法。 到目前为止一切顺利,但是让我们检查一下RecyclerView已经有一些数据并且我们的服务返回的结果与上一次相同的情况下,此更新操作的外观是什么样的:

Image for post

Yack! RecyclerView didn’t have enough information to render the new list properly and we got this “blinking” effect as the result. But as you probably know, RecyclerView provides a lot of ways for dealing with such cases and one of them is DifUtil. In short, it allows you to define a set of comparator functions that can be used by RecyclerView.Adapter to determine whether some item changed its content (or position) and make a “to redraw / not to redraw” decision based on that. Now it’s time to integrate this extra feature into our adapter.

废话! RecyclerView没有足够的信息来正确呈现新列表,因此我们得到了“闪烁”效果。 但您可能知道, RecyclerView提供了许多处理此类情况的方法,其中一种就是DifUtil 。 简而言之,它允许您定义一组比较器函数, RecyclerView.Adapter可以使用这些比较器函数来确定某些项目是否更改了其内容(或位置),并基于此做出“重绘/不重绘”决定。 现在是时候将该额外功能集成到我们的适配器中了。

The simplest solution would be to extend the existing updateData(..) function to do something like this:

最简单的解决方案是扩展现有的updateData(..)函数以执行以下操作:

fun updateData(newItems: List<RecyclerItem>) {
val oldItems = this.data.copy()
val callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldItems.size
override fun getNewListSize(): Int = newItems.size
override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: RecyclerItem = oldItems[oldItemPosition]
val newItem: RecyclerItem = newItems[newItemPosition]
// TODO: compare 2 items
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: RecyclerItem = oldItems[oldItemPosition]
val newItem: RecyclerItem = newItems[newItemPosition]
// TODO: compare 2 items
}
} val diffResult = DiffUtil.calculateDiff(callback)
this.items.clear()
this.items.addAll(newItems)
diffResult.dispatchUpdatesTo(this)
}

This approach should do the trick but there is one caveat here: DiffUtil’s calculations may be relatively resources-consuming so the best practice is to do all the math in a background thread. Of course, we can do that manually, but fortunately, there is a better option — ListAdapter. It does exactly what we need automatically so let’s change our base class from RecyclerView.Adapter to ListAdapter as follows:

这种方法应该可以解决问题,但这里有一个警告:DiffUtil的计算可能会消耗大量资源,因此最佳实践是在后台线程中进行所有数学运算。 当然,我们可以手动执行此操作,但幸运的是,还有一个更好的选择— ListAdapter 。 它正是我们自动需要的功能,因此让我们将基类从RecyclerView.Adapter更改为ListAdapter ,如下所示:

class DataBindingRecyclerAdapter : 
ListAdapter<RecyclerItem, BindingViewHolder>(DiffCallback()) {
override fun getItemViewType(position: Int): Int {
return getItem(position).layoutId
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, viewType, parent, false)
return BindingViewHolder(binding)
}
override fun onBindViewHolder(
holder: BindingViewHolder,
position: Int
) {
getItem(position).bind(holder.binding)
holder.binding.executePendingBindings()
}
}

Two important details:

两个重要的细节:

  • ListAdapter already contains a backing collection for its items so we no longer need to define mutableListOf<RecyclerItem> variable + we can get rid of the getItemCount(): Int override. Perfect! The less code is the better.

    ListAdapter已经包含了其项目的后备集合,因此我们不再需要定义mutableListOf<RecyclerItem>变量+我们可以摆脱getItemCount(): Int重写。 完善! 代码越少越好。

  • ListAdapter requires aDifUtil.ItemCallback constructor parameter.

    ListAdapter需要一个DifUtil.ItemCallback构造函数参数。

The last thing we need to do is to implement this DifUtil.ItemCallback. While it’s a trivial task for simple adapters supporting single item types (here’s an example from the official Google samples), it may be not so easy for our universal adapter which operates by RecyclerItems whose data item can belong to any type. And what makes things even more complicated — we can have an unlimited amount of different types within a single adapter. So the only viable solution here is to provide the users of this adapter with a possibility to provide their own comparators for the types they’re going to use. Here’s the main interface we’re going to use for doing these comparisons:

我们需要做的最后一件事是实现此DifUtil.ItemCallback 。 对于支持单个项目类型的简单适配器而言,这是一项微不足道的任务( 这是来自Google官方示例的示例),但对于我们的通用适配器而言,它并不是那么容易的事情,它由RecyclerItem操作,其data项可以属于任何类型。 事情变得更加复杂的原因是-在一个适配器中我们可以拥有无​​限数量的不同类型。 因此,这里唯一可行的解​​决方案是为该适配器的用户提供为其使用的类型提供自己的比较器的可能性。 这是我们将用于进行这些比较的主要界面:

interface RecyclerItemComparator {
fun isSameItem(other: Any): Boolean
fun isSameContent(other: Any): Boolean
}

As you’ve probably noticed, this interface is looking very similar to the ones provided by DiffUtil , the only difference is that it has only one input parameter instead of two. So it is something between Java’s equals and DiffUtil.ItemCallback. Now it’s time to implement actual DiffCalback we’re already passing to DataBindingRecyclerAdapter's constructor:

您可能已经注意到,该接口看起来与DiffUtil提供的接口非常相似,唯一的区别是它只有一个输入参数,而不是两个。 因此,它介于Java的equalsDiffUtil.ItemCallback 。 现在是时候实现实际的DiffCalback我们已经传递给了DataBindingRecyclerAdapter的构造函数:

private class DiffCallback : DiffUtil.ItemCallback<RecyclerViewItem>() {
override fun areItemsTheSame(
oldItem: RecyclerViewItem,
newItem: RecyclerViewItem
): Boolean {
val oldData = oldItem.data
val newData = newItem.data
// Use appropriate comparator's method if both items implement the interface
// and rely on plain 'equals' otherwise
return if (oldData is RecyclerItemComparator
&& newData is RecyclerItemComparator
) {
oldData.isSameItem(newData)
} else oldData == newData

}
override fun areContentsTheSame(
oldItem: RecyclerViewItem,
newItem: RecyclerViewItem
): Boolean {
val oldData = oldItem.data
val newData = newItem.data
return if (oldViewModel is RecyclerItemComparator
&& newViewModel is RecyclerItemComparator
) {
oldViewModel.isSameContent(newViewModel)
} else oldViewModel == newViewModel

}
}

That’s it. Our adapter will now try to compare RecyclerItems using the comparator we’ve just created and rely on simple equals otherwise. Now it’s time to implement RecyclerItemComparator in the UserItemViewModel:

而已。 现在,我们的适配器将尝试使用刚刚创建的比较器比较RecyclerItem否则将依赖简单的equals 。 现在是时候在UserItemViewModel实现RecyclerItemComparator了:

class UserItemViewModel(val user: User) : RecyclerItemComparator {

override fun isSameItem(other: Any): Boolean {
if (this === other) return true
if (javaClass != other.javaClass) return false
other as UserItemViewModel
return this.user.id == other.user.id
}
override fun isSameContent(other: Any): Boolean {
other as UserItemViewModel
return this.user == other.user
}
}

The original “blinking” problem has finally gone and the adapter is now capable of handling more complicated cases like inserting a new item to the end of the previous list + position changes without extra redrawings:

原来的“闪烁”问题终于消失了,适配器现在能够处理更复杂的情况,例如在前一个列表的末尾插入新项目+更改位置而无需额外重画:

Image for post

Yay! Much better. And note that you don’t have to implement RecyclerItemComparator every time you use the adapter. This step is absolutely optional and can be skipped in many cases:

好极了! 好多了。 请注意,您不必每次使用适配器都实现RecyclerItemComparator 。 此步骤是绝对可选的,在许多情况下可以跳过:

  • You don’t update the list very often and/or don’t care about possible blinkings

    您不经常更新列表和/或不在乎可能的闪烁
  • You use Kotlin’s Data classes inside RecyclerItems (== fallback in the DiffCallback)

    您可以在RecyclerItem使用Kotlin的Data类( DiffCallback == fallback)

  • You use classes which already override equals / hashCode properly (== fallback in the DiffCallback)

    您使用已经正确覆盖equals / hashCode类( DiffCallback == fallback)

常问问题 (FAQ)

Q: How do I handle clicks with this adapter? I can’t easily access my RecyclerView.ViewHolders.

问:如何使用此适配器处理点击? 我无法轻松访问 RecyclerView.ViewHolder

A: DataBinding to the rescue. Again. First, define a new method in your data/item/view model:

答:抢救数据绑定。 再次。 首先,在您的数据/项目/视图模型中定义一个新方法:

class UserItemViewModel(val user: User) {
fun onClick() {
// handling logic
}
}

And now you just need to extend your item’s XML a little bit:

现在,您只需要扩展项目的XML:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="viewModel"
type="com.package.UserItemViewModel" />
</data>
<LinearLayout
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.onClick()}"
android:orientation="vertical">
<!-- Other content -->
</LinearLayout>
</layout>

What if you also need to deliver click events from list item entities to a parent or root ViewModel? There are many options available:

如果还需要将单击事件从列表项实体传递到父视图或根ViewModel,该怎么办? 有很多可用的选项:

  • RxJava: use Subject + Observable pair to emit click events and observe them from the parent.

    RxJava :使用Subject + Observable对发出点击事件,并从父节点观察它们。

  • Coroutines: use Channel or Flow, everything else is the same as in the case of RxJava.

    协程 :使用ChannelFlow ,其他一切与RxJava相同。

  • Callbacks: the simplest one, you can define a callback interface or lambda, create a mutable variable of this type in your item class and then assign a listener from the root view model.

    回调 :最简单的方法是,您可以定义一个回调接口或lambda,在item类中创建此类型的可变变量,然后从根视图模型分配一个侦听器。

Q: How do I update data in the adapter? What if I need to do partial update, e.g. to update/replace a single element? Should I create new methods for that in the adapter or create its subclass?

问:如何更新适配器中的数据? 如果我需要进行部分更新,例如更新/替换单个元素,该怎么办? 我应该在适配器中为此创建新方法还是创建其子类?

A: The main and the only way to update the content of the adapter is to call its updateData(list: List<RecyclerItem>) method (indirectly through the BindingAdapter from Part 1 post). This is by design. Manipulate by collections of your natural domain entities (like simple User POJO or item view models similar to UserItemViewModel) and then convert them to RecyclerItems right before exposing the final List<RecyclerItem> to your UI layer. Let the adapter and DiffUtil to do the rest, RecyclerView is smart enough to avoid unnecessary UI operations as long as you provide it with the information it needs.

:更新适配器内容的主要且唯一的方法是调用其updateData(list: List<RecyclerItem>)方法(通过第1部分中的BindingAdapter间接BindingAdapter )。 这是设计使然。 通过对自然域实体的集合进行操作(例如简单的User POJO或类似于UserItemViewModel项目视图模型),然后在将最终List<RecyclerItem>暴露给UI层之前将它们转换为RecyclerItems 。 让适配器和DiffUtil完成其余工作, RecyclerView足够聪明,只要您为其提供所需的信息,就可以避免不必要的UI操作。

Q: How can I access the data stored in List<RecyclerItem>? RecyclerItem's data parameter has Any type.

问:如何访问存储在 List<RecyclerItem> 的数据 RecyclerItem data 参数具有 Any 类型。

A: You don’t have to store two collections in your view models (like List<User> and List<RecyclerItem>) if you need access to the original data format you had before converting it into RecyclerItems, a list of RecyclerItems should suffice in most situations. A simple mapping like this one should be enough:

:你不必存储在您的视图模型两个集合(如List<User>List<RecyclerItem>如果你需要访问你把它转换成之前所拥有的原始数据格式RecyclerItem S,列表RecyclerItem小号在大多数情况下应该足够了。 像这样的简单映射就足够了:

val data = mutableData<List<RecyclerItem>>()fun doSomethingWithUsers() {
val users: List<User> =
data.value.orEmpty()
.map { it.data }
.filterIsInstance<User>()
// TODO process "users"
}

That’s basically all I was going to share with the community on this topic. Any feedback would be really appreciated and as I’ve already pointed out in the beginning, I prepared a simple library called DataBindingRecyclerAdapter so you can easily add it to your project and check in practice. I might extend it with extra features, documentation & samples in the future but for now, it is super simple and only contains the code listed in Part 1 & Part 2 posts.

基本上,这就是我要与社区分享的所有内容。 非常感谢您提供任何反馈,并且正如我在开始时已经指出的那样,我准备了一个名为DataBindingRecyclerAdapter的简单库,因此您可以轻松地将其添加到项目中并进行实践检查。 我可能会在将来使用其他功能,文档和示例进行扩展,但就目前而言,它非常简单,仅包含第1部分和第2部分中列出的代码。

Thanks for reading and good luck :-)

感谢您的阅读和好运:-)

翻译自: https://medium.com/@fraggjkee/recyclerview-2020-a-modern-way-of-dealing-with-lists-in-android-using-databinding-part-2-df69f0a741f8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值