使用AsyncListUtil优化RecyclerView

来源:互联网 发布:五五开的淘宝店叫什么 编辑:程序博客网 时间:2024/06/07 13:29

大家好,今天为大家推荐的是来自极光推送的Hevin同学所翻译的文章,重点是介绍AsyncListUtil这个类的使用,它在Android API 23开始就被加入到support.v7当中了,你可能很少看到有人提起过它,但实际上却是非常实用的工具类,至于怎么使用,看完本文你就知道。当然除了AsyncListUtil之外,DiffUtil这个类也是值得你了解一下的,本文虽然没有提及,有兴趣的读者可以自行拓展。文章的示例代码虽是用Kotlin编写,但不妨碍Java的同学阅读。

  • 原文地址:https://zhuanlan.zhihu.com/p/29491805


AsyncListUtil 是一个用于异步内容加载的类,在 Android API 23 时被加入到 support.v7 当中。不过好像很多人对它还并不了解,网上也没有太多相关的资料。今天这里就来介绍下 AsyncListUtil 的用法。

首先,AsyncListUtil 通常和 RecyclerView 搭配使用的。其能够在后台线程中加载 Cursor 数据,同时保持 UI 和缓存的同步来实现更好的用户体验。不过 AsyncListUtil 是通过单个线程加载数据,因此适用于从二级存储(比如硬盘)中加载数据,而不适用于从网络加载数据的情况。

RecyclerView的结构

相信绝大部分 Android 开发者对此都已经非常熟悉了。

RecyclerView + AsyncListUtil的结构

可以看到 AsyncListUtil 是通过 AsyncListUtil.ViewCallback 来判断当前数据可见的范围,再通过 AsyncListUtil.DataCallback 从后台加载所需的数据,并在加载完成时通知 AsyncListUtil.ViewCallback。

因此要使用 AsyncListUtil,首先需要继承实现 AsyncListUtil.DataCallback 和 AsyncListUtil.ViewCallback 这两个抽象类。

下面我们通过代码来看看实际要怎样实现?先上效果图:

数据

作者实现了一个简单的Python脚本生成了 100,000 条数据并存放在 SQLite 数据库中。每一条数据都有 id, title 和 content 三个属性。其中的 title 和 content 都是通过DWYL’s english-words repository 随机生成。

ItemSource

class Item(var title: String, var content: String)interface ItemSource {    fun getCount(): Int    fun getItem(position: Int): Item    fun close()}

定义 SQLiteItemSource 来从 SQLite 中获取数据:

class SQLiteItemSource(val database: SQLiteDatabase) : ItemSource {    private var _cursor: Cursor? = null    private val cursor: Cursor        get() {            if (_cursor == null || _cursor?.isClosed != false) {                _cursor = database.rawQuery("SELECT title, content FROM data", null)            }            return _cursor ?: throw AssertionError("Set to null or closed by another thread")        }    override fun getCount() = cursor.count    override fun getItem(position: Int): Item {        cursor.moveToPosition(position)        return Item(cursor)    }    override fun close() {        _cursor?.close()    }}private fun Item(c: Cursor): Item = Item(c.getString(0), c.getString(1))

Callbacks

为了创建 AsyncListUtil,我们需要传入 DataCallback 和 ViewCallback。

首先让我们实现 DataCallback:

private class DataCallback(val itemSource: ItemSource) : AsyncListUtil.DataCallback<Item>() {    override fun fillData(data: Array<Item>?, startPosition: Int, itemCount: Int) {        if (data != null) {            for (i in 0 until itemCount) {                data[i] = itemSource.getItem(startPosition + i)            }        }    }    override fun refreshData(): Int = itemSource.getCount()    fun close() {        itemSource.close()    }}

DataCallback 是用来为 AsyncListUtil 提供数据访问,其中所有方法都会在后台线程中调用。

其中有两个方法必需要实现:

  • fillData(data, startPosition, itemCount) - 当 AsyncListUtil 需要更多数据时,将会在后台线程调用该方法。

  • refreshData() - 返回刷新后的数据个数。

再实现 ViewCallback:

private class ViewCallback(val recyclerView: RecyclerView) : AsyncListUtil.ViewCallback() {    override fun onDataRefresh() {        recyclerView.adapter.notifyDataSetChanged()    }    override fun getItemRangeInto(outRange: IntArray?) {        if (outRange == null) {            return        }        (recyclerView.layoutManager as LinearLayoutManager).let { llm ->            outRange[0] = llm.findFirstVisibleItemPosition()            outRange[1] = llm.findLastVisibleItemPosition()        }        if (outRange[0] == -1 && outRange[1] == -1) {            outRange[0] = 0            outRange[1] = 0        }    }    override fun onItemLoaded(position: Int) {        recyclerView.adapter.notifyItemChanged(position)    }}

AsyncListUtil 通过 ViewCallback 主要是做两件事:

  • 通知视图数据已经更新(onDataRefresh);

  • 了解当前视图所展示数据的位置,从而确定什么时候获取更多数据或释放掉目前不在窗口内的旧数据(getItemRangeInto);

接下来实现 ScrollListener 来调用 AsyncListUtil 的 onRangeChanged() 方法:

private class ScrollListener(val listUtil: AsyncListUtil<in Item>) : RecyclerView.OnScrollListener() {    override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {        listUtil.onRangeChanged()    }}

Adapter

至此,AsyncListUtil 所需要的组件都准备好了,可以来实现我们的 RecyclerView.Adapter 了:

class AsyncAdapter(itemSource: ItemSource, recyclerView: RecyclerView) : RecyclerView.Adapter<ViewHolder>() {    private val dataCallback = DataCallback(itemSource)    private val listUtil = AsyncListUtil(Item::class.java, 500, dataCallback, ViewCallback(recyclerView))    private val onScrollListener = ScrollListener(listUtil)    fun onStart(recyclerView: RecyclerView?) {        recyclerView?.addOnScrollListener(onScrollListener)        listUtil.refresh()    }    fun onStop(recyclerView: RecyclerView?) {        recyclerView?.removeOnScrollListener(onScrollListener)        dataCallback.close()    }    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {        holder?.bindView(listUtil.getItem(position), position)    }    override fun getItemCount(): Int = listUtil.itemCount    override fun onCreateViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder {        val inf = LayoutInflater.from(parent.context)        return ViewHolder(inf.inflate(R.layout.item, parent, false))    }}

其中实例化 AsyncListUtil 时的 500 表示分页大小。

要注意的一点是 listUtil.getItem(position) 在指定 position 对应的数据仍在被加载时会返回 null ,因此需要在 ViewHolder 中处理当 item 为 null 的情况:

class ViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView) {    private val title: TextView? = itemView?.findViewById(R.id.title)    private val content: TextView? = itemView?.findViewById(R.id.content)    fun bindView(item: Item?, position: Int) {        title?.text = "$position ${item?.title ?: "loading"}"        content?.text = item?.content ?: "loading"    }}

这里当 item 为 null 时,就简单的显示 “loading”。

最后,在 Activity 中把所有的这些组合起来:

class MainActivity : AppCompatActivity() {    private lateinit var recyclerView: RecyclerView    private lateinit var adapter: AsyncAdapter    private lateinit var itemSource: SQLiteItemSource    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        recyclerView = findViewById(R.id.recycler)        itemSource = SQLiteItemSource(getDatabase(this, "database.sqlite"))        adapter = AsyncAdapter(itemSource, recyclerView)        recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)        recyclerView.adapter = adapter    }    override fun onStart() {        super.onStart()        adapter.onStart(recyclerView)    }    override fun onStop() {        super.onStop()        adapter.onStop(recyclerView)    }}

完整项目代码可以在 Github 上找到:https://github.com/jasonwyatt/AsyncListUtil-Example

原文链接:https://android.jlelse.eu/how-to-use-asynclistutil-16b5175bb468

微信文章不支持文中的外链,可以跳转到原文继续阅读。