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) 成立,进而直接 returnsubmitList() 方法。新的数据并未更新到列表中。

3 最后

好的,以下是一个使用RecyclerView的基本范例,它展示了一个简单的列表,列表中的每项都包含一个文本和一个图像: 1. 首先,在你的Android Studio项目中添加RecyclerView依赖: ``` implementation 'androidx.recyclerview:recyclerview:1.1.0' ``` 2. 创建一个RecyclerView布局文件和一个列表项布局文件。在这个例子中,我们将使用LinearLayoutManager来实现垂直滚动的列表。 `activity_main.xml`: ``` <?xml version="1.0" encoding="utf-8"?> <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" /> ``` `list_item.xml`: ``` <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp"> <ImageView android:id="@+id/imageView" android:layout_width="48dp" android:layout_height="48dp" android:src="@drawable/ic_launcher_background" /> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:text="List Item" android:textColor="@android:color/black" android:textSize="16sp" /> </LinearLayout> ``` 3. 创建一个列表项ViewHolder类,用于绑定列表项布局文件中的View。 `ListItemViewHolder.java`: ``` import android.view.View; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; public class ListItemViewHolder extends RecyclerView.ViewHolder { private ImageView imageView; private TextView textView; public ListItemViewHolder(@NonNull View itemView) { super(itemView); imageView = itemView.findViewById(R.id.imageView); textView = itemView.findViewById(R.id.textView); } public void bind(ListItem item) { imageView.setImageResource(item.getImageResource()); textView.setText(item.getText()); } } ``` 4. 创建一个列表项数据类,用于存储每一个列表项的数据。 `ListItem.java`: ``` public class ListItem { private int imageResource; private String text; public ListItem(int imageResource, String text) { this.imageResource = imageResource; this.text = text; } public int getImageResource() { return imageResource; } public String getText() { return text; } } ``` 5. 创建一个RecyclerView适配器类,用于绑定列表项数据和ViewHolder。 `ListAdapter.java`: ``` import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; public class ListAdapter extends RecyclerView.Adapter<ListItemViewHolder> { private List<ListItem> listItems; public ListAdapter(List<ListItem> listItems) { this.listItems = listItems; } @NonNull @Override public ListItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.list_item, parent, false); return new ListItemViewHolder(view); } @Override public void onBindViewHolder(@NonNull ListItemViewHolder holder, int position) { holder.bind(listItems.get(position)); } @Override public int getItemCount() { return listItems.size(); } } ``` 6. 在MainActivity中使用RecyclerView并设置适配器。 `MainActivity.java`: ``` import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recyclerView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); List<ListItem> listItems = new ArrayList<>(); listItems.add(new ListItem(R.drawable.ic_launcher_background, "List Item 1")); listItems.add(new ListItem(R.drawable.ic_launcher_background, "List Item 2")); listItems.add(new ListItem(R.drawable.ic_launcher_background, "List Item 3")); ListAdapter adapter = new ListAdapter(listItems); recyclerView.setAdapter(adapter); } } ``` 这样就完成了一个简单的使用RecyclerView的列表。当你运行这个应用程序时,你应该可以看到一个包含三个列表项的列表,每一个列表项都包含一个图像和一个文本。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值