RecyclerView
is undoubtedly one of the most important and widely used UI widgets available in Android SDK nowadays, and it’s really hard to imagine a modern application with no lists at all. There are hundreds of articles about this component and there is a lot of great libraries simplifying its usage provided by the community. Nevertheless, I’d like to add my 5 cents here and discuss traditional approaches of working with lists, what problems they bring, and how they can potentially be solved by using Android Data Binding Library.
毫无疑问, RecyclerView
是当今Android SDK中最重要且使用最广泛的UI小部件之一,而且很难想象根本没有列表的现代应用程序。 关于该组件的文章有数百篇,并且有很多很棒的库可以简化社区提供的使用。 不过,我想在这里加5美分,讨论使用列表的传统方法,它们带来的问题以及如何使用Android 数据绑定库解决这些问题。
Let’s take a look at a canonical implementation of aRecyclerView.Adapter
, shamelessly borrowed from the official docs:
让我们看一下RecyclerView.Adapter
的规范实现,该RecyclerView.Adapter
从官方文档中 RecyclerView.Adapter
地借来的:
class MyAdapter : RecyclerView.Adapter() {
class MyViewHolder(
val textView: TextView
) : RecyclerView.ViewHolder(textView) private var data: List = emptyList()override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val textView = inflater.inflate(R.layout.my_text_view, parent, false) as TextView
return MyViewHolder(textView)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.textView.text = data[position]
}
override fun getItemCount() = myDataset.size fun updateData(list: List) {
data = list
notifyDataSetChanged()
}
}
Quite a lot of code for such a simple task as rendering a list of strings on screen, isn’t it? But what’s even more important, this adapter is tightly coupled to a single data type (String
) and XML layout. Essentially, we often have a 1 : 1 mapping between the number of lists in the app and the number of RecyclerView.Adapter
implementations: we have to create a new adapter (including RecyclerView.ViewHolder
) pretty much every time we’re dealing with a new list. What’s even worse, a significant part of these implementations consists of identical boilerplate code which is only there to match the contract of the RecyclerView.Adapter
abstract class. Pretty annoying I’d say. But let’s continue with this adapter anyway and try to link it to a hypothetical screen based on Fragment
or Activity
.
诸如在屏幕上呈现字符串列表之类的简单任务的代码很多,不是吗? 但是更重要的是,该适配器与单个数据类型( String
)和XML布局紧密耦合。 从本质上讲,我们通常在应用程序中的列表数量与RecyclerView.Adapter
实现的数量之间具有1:1的映射:每次处理一个适配器时,我们几乎都要创建一个新的适配器(包括RecyclerView.ViewHolder
)。新清单。 更糟糕的是,这些实现中的很大一部分由相同的样板代码组成,这些代码仅与RecyclerView.Adapter
抽象类的协定相匹配。 我会说很烦。 但是无论如何,让我们继续使用此适配器,并尝试将其链接到基于Fragment
或Activity
的假设屏幕。
Luckily, the days when writing all the business logic right inside Activities and Fragments was a common practice are gone and a modern Android developer would extract it into a separate entity like Presenter
or ViewModel
:
幸运的是,不再存在在Activity和Fragments中编写所有业务逻辑的普遍习惯的时代,现代的Android开发人员会将其提取到一个独立的实体中,例如Presenter
或ViewModel
:
class MyViewModel : ViewModel() {
val data = MutableLiveData()
init {
loadData()
}
private fun loadData() {
// ...fetch data from API / DB and put it to `data`
data.value = ...
}
}
View models & presenters do not know anything about RecyclerView
s, hence they can’t update the UI directly. Instead, we need to write some extra code in the UI layer to complete the chain:
视图模型和演示者对RecyclerView
并不了解,因此他们无法直接更新UI。 相反,我们需要在UI层中编写一些额外的代码来完成该链:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModels()
private val adapter = MyAdapter() ... override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
viewModel.data.observe(viewLifecycleOwner, Observer {
adapter.updateData(it)
})
}
}
Quite a typical code and I bet most of us have already seen or even written something very similar many times before. What problems does it have? First of all, we have to create an adapter manually and store a reference to it so it can be updated if data set changes. Second, we have to set up observing of the data exposed by View Model and react to these changes by updating the underlying adapter’s data set. While both these issues do not bring any architectural or ideological issue, they still lead to writing extra boilerplate code in our UI layer, we have to manually link the two worlds — plain & natural data flowing in business logic layer (View Models, Repositories, etc.) and UI handled by Views, Fragments or Activities.
相当典型的代码,我敢打赌,我们大多数人以前已经看过甚至写了很多非常相似的东西。 它有什么问题? 首先,我们必须手动创建一个适配器并存储对其的引用,以便在数据集发生更改时可以对其进行更新。 其次,我们必须设置对View Model公开数据的观察,并通过更新基础适配器的数据集来对这些更改做出React。 尽管这两个问题都没有带来任何体系结构或意识形态问题,但它们仍然导致在我们的UI层中编写额外的样板代码,但我们必须手动链接这两个世界-业务逻辑层中的纯自然数据流(查看模型,存储库,等),以及由视图,片段或活动处理的用户界面。
Well, we’ve found the following areas for improvement in the “classic” way of working with Lists & Adapters in Android:
好吧,我们发现了以下方面可以改进“经典”的Android列表和适配器工作方式:
We have to create new & new adapters for almost every single individual
RecyclerView
in the project. These adapters can’t always be reused and contain a lot of boilerplate code.我们必须为项目中几乎每个单独的
RecyclerView
创建新的和新的适配器。 这些适配器不能总是重复使用,并且包含许多样板代码。It would be a bad idea to store
RecyclerView
's adapters right inside view models for lots of reasons (separation of concerns, view models reusability) so we have to expose domain-level data from View Models / Presenters and manually convert it to its UI representation by using adapters. This leads to writing extra code in Fragments and Activities + UI layer becomes the one who performs actual mapping, we expose domain (POJOs, Data Classes, etc.) format to UI layer so it can do the conversion.由于种种原因(关注点分离,视图模型可重用性),将
RecyclerView
的适配器存储在视图模型内部将是一个坏主意,因此我们必须公开视图模型/演示者的域级数据并将其手动转换为UI使用适配器表示。 这导致在Fragments and Activities中添加了额外的代码+ UI层成为执行实际映射的人,我们将域(POJO,数据类等)格式暴露给UI层,以便可以进行转换。
As some of you probably know, there is a solution to both of these problems and it’s called Android Data Binding framework. Let’s get started…!
你们中有些人可能知道,对于这两个问题都有解决方案,它称为Android数据绑定框架。 让我们开始吧…!
To begin with, let’s create a new data
class which will be the only format used by our future universal Adapter:
首先,让我们创建一个新的data
类,它将是我们将来的通用适配器使用的唯一格式:
data class RecyclerItem(
val data: Any,
@LayoutRes val layoutId: Int,
val variableId: Int
) {
fun bind(binding: ViewDataBinding) {
binding.setVariable(variableId, data)
}
}
data will hold… your data. It can also be treated as a “view model of individual list item”. It can either be something as simple as a plain POJO or something more complex with actual business logic, our adapter will not care about that and it is up to you to decide depending on the use case.
数据将保存…您的数据。 也可以将其视为“单个列表项的视图模型”。 它可以是像普通的POJO一样简单的东西,也可以是具有实际业务逻辑的更复杂的东西,我们的适配器将不在乎,您可以根据用例来决定。
layoutId — this is the XML file containing layout for rows of the list. This is where our adapter will try to find variableId to initialize it with the value of data. Essentially, this is a reference to a layout that will be used to render the data.
layoutId —这是XML文件,其中包含列表行的布局。 这是我们的适配器将尝试查找variableId以使用data值对其进行初始化的地方。 本质上,这是对将用于呈现数据的布局的引用。
variableId contains the name of the variable which will be used in your XML layout inside tag. We’ll discuss this in more detail below. Also, check out the Data Binding : Dynamic Variables docs for more details.
variableId包含变量的名称,该变量将在XML布局中的 标记中使用。 我们将在下面更详细地讨论。 另外,请查看“ 数据绑定:动态变量”文档以获取更多详细信息。
Now it’s time to implement the universal DataBinding adapter itself:
现在是时候实现通用DataBinding适配器本身了:
class RecyclerViewAdapter : RecyclerView.Adapter() {
private val items = mutableListOf()
override fun getItemCount(): Int {
return items.size
}
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()
}
fun updateData(newItems: List) {
this.items.clear()
this.items.addAll(newItems)
notifyDataSetChanged()
}
private fun getItem(position: Int): RecyclerItem {
return items[position]
}
}
class BindingViewHolder(
val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root)
That’s basically it, the MVP version of our universal adapter is ready. Let’s quickly discuss what’s going on here.
基本上就可以了,我们的通用适配器的MVP版本已准备就绪。 让我们快速讨论一下这里发生了什么。
getItemViewType
: as you probably know, this callback is typically used to support multiple view types within singleRecyclerView
& adapter. We simply returnlayoutId
from ourRecyclerItem
and this trivial action allows our adapter to automatically handle different view types. We just use the generated XML layout’s ID as a “view type” identifier.getItemViewType
:您可能知道,此回调通常用于在单个RecyclerView
&适配器中支持多种视图类型。 我们只需从RecyclerItem
返回layoutId
,这个简单的操作就可以使我们的适配器自动处理不同的视图类型。 我们只使用生成的XML布局的ID作为“视图类型”标识符。onCreateViewHolder
: most of the data binding magic happens here. We use DataBindingUtil to dynamically create a new ViewDataBinding based on XML layout ID. All data-binding enabled XMLs have a generated class derived from this base class. We don’t know the name of this class here but we know that it will be generated for the givenlayoutId
and it will inheritViewDataBinding
.onCreateViewHolder
:大多数数据绑定魔术都发生在这里。 我们使用DataBindingUtil根据XML布局ID动态创建一个新的ViewDataBinding 。 所有启用了数据绑定的XML都有一个从该基类派生的生成的类。 我们在这里不知道此类的名称,但是我们知道它将为给定的layoutId
生成,并且它将继承ViewDataBinding
。onBindViewHolder
: here we need to link ourViewDataBinding
withviewModel
. Every data binding class has theserVariable()
method which accepts variable ID and data ofObject
type. As you might have guessed, these are ourvariableId
andviewModel
from theRecyclerItem
.onBindViewHolder
:这里我们需要将ViewDataBinding
与viewModel
链接。 每个数据绑定类都有serVariable ()
方法,该方法接受变量ID和Object
类型的数据。 您可能已经猜到了,这些是RecyclerItem
中的我们的variableId
和viewModel
。
As we mentioned earlier, we’d like to avoid writing any glue code in our Fragments and Activities so the last step is to create a BindingAdapter
which can help us to do everything with just 1 line of code in Fragment or Activity XML:
如前所述,我们希望避免在Fragments和Activity中编写任何胶水代码,因此最后一步是创建BindingAdapter
,它可以帮助我们仅用Fragment或Activity XML中的一行代码来完成所有工作:
@BindingAdapter("items")
fun setRecyclerViewItems(
recyclerView: RecyclerView,
items: List?
) {
var adapter = (recyclerView.adapter as? RecyclerViewAdapter)
if (adapter == null) {
adapter = RecyclerViewAdapter()
recyclerView.adapter = adapter
}
adapter.updateData(items.orEmpty())
}
Now it’s time to see how all of that works together in practice. Let’s say we need to implement a screen displaying a list of Users. Here’s how your POJO class may look like:
现在是时候看看所有这些在实践中如何协同工作了。 假设我们需要实现一个显示用户列表的屏幕。 这是您的POJO类的样子:
data class User(
val id: Int,
val firstName,
val lastname
)
Now let’s create a layout which will be used for individual rows:
现在,让我们创建一个用于单个行的布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
name="user"
type="com.package.User" />
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}" />
layout>
Nothing new here for those who are familiar with the Data Binding framework. Now back to our view model: we still need to fetch Users and then “put” them into a RecyclerView somehow.
对于熟悉数据绑定框架的人来说,这里没有什么新鲜的。 现在回到我们的视图模型:我们仍然需要获取用户,然后以某种方式将其“放入” RecyclerView。
class MyViewModel : ViewModel() {
val data = MutableLiveData()
init {
loadData()
}
private fun loadData() {
SomeService.getUsers { users ->
data.value = users.map { it.toRecyclerItem() }
}
}
}
As you see, now we have List
instead of something like List
and we use user.toRecyclerItem()
method to convert our data classes into something which can be recognized by RecyclerView
— RecyclerItem
. We don’t want to spoil our POJOs with logic which belongs to another layer of abstraction (UI) so let’s instead create a private extension in the file holding the ViewModel
:
如您所见,现在有了List
而不是类似List
的东西,并且我们使用user.toRecyclerItem()
方法将数据类转换为RecyclerView
可以识别的东西— RecyclerItem
。 我们不想用属于另一层抽象(UI)的逻辑破坏POJO,因此让我们在保存ViewModel
的文件中创建一个私有扩展:
private fun User.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.user,
layoutId = R.layout.item_user)
That’s the only “glue” code we need to write in order to bridge the gap between ViewModel’s natural data format (list of Users) and RecyclerView
. The last step we need to do is to render the List
in our UI. Thanks to DataBinding and the BindingAdapter
we created earlier, all of that can be done in XML:
那是我们需要编写的唯一“胶水”代码,以弥合ViewModel的自然数据格式(用户列表)和RecyclerView
之间的差距。 我们需要做的最后一步是在UI中呈现List
。 多亏了我们之前创建的DataBinding和BindingAdapter
,所有这些都可以用XML完成:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
name="viewModel"
type="com.package.MyViewModel" />
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{viewModel.data}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
The final step would be to assign your actual ViewModel
to the viewModel
variable from above:
最后一步是从上面将实际的ViewModel
分配给viewModel
变量:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentMyScreenBinding.inflate(inflater, container, false)
.also {
it.viewModel = viewModel
it.lifecycleOwner = viewLifecycleOwner
}
.root
}
}
That’s it and that’s the only code you need to write in your Activities or Fragments when you use DataBinding and the “adapterless” approach we’re discussing in this article. Pretty nice, huh? :-)
就这样,这是当您使用DataBinding和我们在本文中讨论的“无适配器”方法时,只需在“活动”或“片段”中编写的唯一代码。 很好,是吗? :-)
Let’s revisit what we’ve done so far to check whether it is any better than the original version with “1 adapter for 1 list”.
让我们重温到目前为止所做的工作,以检查它是否比带有“ 1个适配器用于1个列表”的原始版本更好。
We no longer need to create new adapters. Ever. With minor additions, this
RecyclerViewAdapter
can be the only adapter in your project.我们不再需要创建新的适配器。 曾经 除了少量添加,此
RecyclerViewAdapter
可以是项目中唯一的适配器。Our Fragments and Activities no longer need to worry about adapters and data observing. The only thing they need to do is to assign a
viewModel
to the corresponding XML layout and this looks & feels pretty natural. Everything else is handled in XML with just a couple lines of code.我们的片段和活动不再需要担心适配器和数据观察。 他们唯一需要做的就是将
viewModel
分配给相应的XML布局,这看起来和感觉都很自然。 其他所有内容都只用几行代码就可以用XML处理。We no longer need to expose domain data format from View Models to Activities and Fragments. Instead, you just need to provide a generic list of
RecyclerItem
s which hides actual data format from the UI andRecyclerItem
can be handled by the UI directly with no extra transformations (thanks to theDataBindingAdapter
andRecyclerViewAdapter
it uses under the hood).我们不再需要将域数据格式从视图模型公开到活动和片段。 相反,您只需要提供一个
RecyclerItem
的通用列表即可从UI隐藏实际数据格式,并且RecyclerItem
可以直接由UI处理而无需进行额外的转换(这要归功于它在RecyclerViewAdapter
使用的DataBindingAdapter
和RecyclerViewAdapter
)。You don’t need to spoil your POJOs or recycler item View Models by extending some base class, you don’t even need to implement any extra interface. The POJOs or view models you use for
RecyclerView
look like regular classes. You just need to write a mapper somewhere where it makes the most sense for you (static or extension function returning aRecyclerItem
).您不需要通过扩展某些基类来破坏POJO或回收器项目的View模型,甚至不需要实现任何额外的接口。 用于
RecyclerView
的POJO或视图模型看起来像常规类。 您只需要在最适合您的位置编写一个映射器(静态或扩展函数返回RecyclerItem
)。There is no need to extend the adapter every time when a new item type is added. You can easily support as many view types in a single list as you want, you just need to define mappings from your data to
RecyclerItem
:添加新项目类型时,无需每次都扩展适配器。 您可以轻松地在单个列表中支持任意数量的视图类型,只需定义从数据到
RecyclerItem
映射即可:
val data = MutableLiveData()fun loadData() {
val users: List = ...fetch users...
val admins: List = ...fetch admins...
data.value =
users.map { it.toRecyclerItem()) }
+ admins.map { it.toRecyclerItem() }
}fun User.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.user,
layoutId = R.layout.item_user
)fun Admin.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.admin,
layoutId = R.layout.item_admin
)
Much better than the new MergeAdapter
, isn’t it? :-)
比新的MergeAdapter
,不是吗? :-)
What’s next? As I mentioned earlier, the current implementation is a bit simplified: it will work just fine for relatively static cases with rare updates of the content. But for more dynamic scenarios it would be great to also integrate DifUtil
and move its calculations to a background thread. I’ll try to uncover this topic (along with some other improvements) in the next article.
下一步是什么? 正如我前面提到的,当前的实现方式略有简化:它在内容很少更新的相对静态的情况下也可以正常工作。 但是对于更动态的场景,最好还集成DifUtil
并将其计算移至后台线程。 我将在下一篇文章中尝试发现该主题(以及其他一些改进)。
Thanks for reading and please let me know what do you think about this approach ;-)
感谢您的阅读,请让我知道您对这种方法有何看法;-)
UPD: Part 2 of this series can be found here
UPD :本系列的第2部分可在此处找到
翻译自: https://medium.com/@fraggjkee/recyclerview-2020-a-modern-way-of-dealing-with-lists-in-android-using-databinding-d97abf5fb55f