Android 筑基——使用 recyclerview 库的 ListAdapter 需要注意的地方
1 前言
在查看 google 的开源项目 sunflower 时看到使用到了 ListAdapter
,开始以为是之前对应 ListView
的那个 ListAdapter
,实际上是 recyclerview 库里提供的。自己也就跟着使用到了项目中。当然,经历了一些问题后,才比较好地掌握了 ListAdapter
的正确使用方法。
本文主要会回答如下的问题:
- 为什么提供同样引用的集合给
ListAdapter
后,没有任何反映? - 为什么使用
ListAdapter
来实现数据的增加,删除,更新,会出现不按预期操作的情况? DiffUtil.ItemCallback
抽象类的两个抽象方法怎么用?- 使用
ListAdapter
如何获取数据提交成功的回调?
2 正文
2.1 基本使用
通过一个列表展示条目,加载更多展示数据的增加操作,选择删除展示删除操作。
我们的例子是这样的:
就是展示一系列带数字的卡片。
下面我们先过一下代码:
首先是数据类:
class Item(val number: Int, val colorRes: Int, var selected: Boolean = false) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Item
if (number != other.number) return false
if (colorRes != other.colorRes) return false
if (selected != other.selected) return false
return true
}
override fun hashCode(): Int {
var result = number
result = 31 * result + colorRes
result = 31 * result + selected.hashCode()
return result
}
}
其次是条目的布局文件 recycle_item.xml
:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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"
android:id="@+id/cv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground"
app:cardElevation="0dp"
app:cardBackgroundColor="@color/cyan_600"
app:cardCornerRadius="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:gravity="center"
tools:text="1"
style="@style/TextAppearance.MaterialComponents.Headline1"
android:id="@+id/tv"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="h,9:16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
然后是适配器的代码:
class MyListAdapter : ListAdapter<Item, MyListAdapter.MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder.from(parent)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(currentList[position], position)
}
class MyViewHolder(private val binding: RecycleItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item, position: Int) {
item.apply {
binding.tv.text = number.toString()
binding.cv.setCardBackgroundColor(ContextCompat.getColor(itemView.context, colorRes))
}
}
companion object {
fun from(parent: ViewGroup): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = RecycleItemBinding.inflate(inflater, parent, false)
return MyViewHolder(binding)
}
}
}
companion object {
val diffCallback = object: DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem == newItem
}
}
}
}
这里需要再次说明的是 ListAdapter
不是原来在 ListView
中使用的那个,而是用于 RecyclerView
的。
这里我们的 MyListAdapter
正是继承于 ListAdapter
。那么,大家可能会想:这个和继承于 RecyclerView.Adapter
有什么区别呢?
看一下代码可以发现一点:ListAdapter
的构造方法需要接收一个 AsyncDifferConfig<T>
类型的参数:
protected ListAdapter(@NonNull AsyncDifferConfig<T> config) {
mDiffer = new AsyncListDiffer<>(new AdapterListUpdateCallback(this), config);
mDiffer.addListListener(mListener);
}
另外,我们不用实现 RecyclerView.Adapter
中的 getItemCount()
这个抽象方法了,ListAdapter
已经帮我们实现了。
最后,看一下主页的布局以及代码:
<?xml version="1.0" encoding="utf-8"?>
<com.scwang.smart.refresh.layout.SmartRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never" />
<com.scwang.smart.refresh.footer.ClassicsFooter
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.scwang.smart.refresh.layout.SmartRefreshLayout>
class MainActivity : AppCompatActivity() {
private lateinit var binding: MainActivityBinding
private lateinit var adapter: MyListAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
setupAdapter()
setupSmartRefreshLayout()
}
private fun setupAdapter() {
adapter = MyListAdapter()
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
binding.recyclerView.addItemDecoration(GridSpaceDecoration.newInstance(10))
binding.recyclerView.adapter = adapter
adapter.submitList(getPageList())
}
private fun setupSmartRefreshLayout() {
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setOnLoadMoreListener {
lastIndex = lastIndex + pageNumber
if (lastIndex >= 50) {
binding.refreshLayout.finishRefreshWithNoMoreData()
return@setOnLoadMoreListener
}
val pageList = getPageList()
val newList = mutableListOf<Item>()
newList.addAll(adapter.currentList)
newList.addAll(pageList)
adapter.submitList(newList)
binding.refreshLayout.finishLoadMore(300)
}
}
private var lastIndex: Int = 1
private var pageNumber = 10
private fun getPageList(): List<Item> {
val colorList = getColorList()
var result = mutableListOf<Item>()
for (i in lastIndex until lastIndex + pageNumber) {
result.add(Item(i, colorList[i % colorList.size]))
}
return result
}
}
这里实现的是上拉加载数据的功能。
需要特别注意的是,向 MyListAdapter
添加数据使用的是
adapter.submitList(getPageList())
而且我们后面会看到,不仅仅是添加数据,包括更新,删除都需要调用这个方法。
通过演示图,可以看到我们加载数据是成功的。看一下代码上是如何处理的:
val pageList = getPageList()
val newList = mutableListOf<Item>()
newList.addAll(adapter.currentList)
newList.addAll(pageList)
adapter.submitList(newList)
第 1 行:拿到新加载好的数据;
第 2 行:创建一个新的空集合;
第 3 行:把原来的数据集合添加到新的空集合里;
第 4 行:把新加载好的数据添加到新的集合里;
第 5 行:调用 submitList
方法提交数据。
到这里,我们想一想:为什么要用一个新的集合来存放数据,而不使用已有的集合呢?也就是说,把上面的代码写成这样:
val pageList = getPageList()
adapter.currentList.addAll(pageList)
adapter.submitList(adapter.currentList)
实测发现,这样写会出现崩溃:
2020-11-14 19:23:03.575 1422-1422/com.example.listadapterstudy E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.listadapterstudy, PID: 1422
java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableCollection.addAll(Collections.java:1112)
at com.example.listadapterstudy.MainActivity$setupSmartRefreshLayout$1.onLoadMore(MainActivity.kt:42)
at com.scwang.smart.refresh.layout.SmartRefreshLayout$5.run(SmartRefreshLayout.java:1727)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
不允许对 adapter.currentList
调用 addAll()
方法。所以,这样是不行的。
或许,有人会想自己持有一个集合,每次拿到新数据后都放到这个集合里面,并用这个集合来提交数据。也就是说,代码修改成这样:
private val list = mutableListOf<Item>()
private fun setupAdapter() {
// 省略一些代码
list.addAll(getPageList())
adapter.submitList(list)
}
private fun setupSmartRefreshLayout() {
binding.refreshLayout.setEnableRefresh(false)
binding.refreshLayout.setOnLoadMoreListener {
// 省略一些代码
val pageList = getPageList()
list.addAll(getPageList())
adapter.submitList(list)
binding.refreshLayout.finishLoadMore(300)
}
}
这次不会报错了,但是看一下测试效果:
可以看到数据没有更新,而我们确实拿到了数据:
新拿到的数据根本没有更新到列表中来。问题可能出在 submitList()
方法上:
public void submitList(@Nullable List<T> list) {
mDiffer.submitList(list);
}
最终调用的是 AsyncListDiffer
类中的 submitList()
方法:
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
if (newList == mList) {
// nothing to do (Note - still had to inc generation, since may have ongoing work)
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
// fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// fast simple first insert
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
// notify last, after list is updated
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
final List<T> oldList = mList;
// 省略一些代码。
}
第一次我们调用 submitList()
方法时,把成员变量集合 list
传递过来,那么 newList
参数就不为 null,在第 25 行,会进入 if
语句,把 newList
赋值给 mList
,也就是把 list
赋值给 mList
。
第二次我们调用submitList()
方法时,仍是把成员变量集合 list
传递过来,这时 mList
就是指向的 list
,所以第 5 行的 if (newList == mList)
成立,进而直接 return
掉 submitList()
方法。新的数据并未更新到列表中。