简评:AsyncListUtil 在 Android API 23 就被加入到 support.v7 当中了,但似乎长久以来都被忽视了,其实在合适的场景中还是挺有用的。
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 上找到:jasonwyatt/AsyncListUtil-Example。
原文:how-to-use-asynclistutil
延伸阅读:
理解 Android 新的依赖方式
RecyclerView 实现快速滚动
现代 Android 开发资源汇总