簡(jiǎn)評(píng):AsyncListUtil 在 Android API 23 就被加入到 support.v7 當(dāng)中了,但似乎長(zhǎng)久以來(lái)都被忽視了,其實(shí)在合適的場(chǎng)景中還是挺有用的。
AsyncListUtil 是一個(gè)用于異步內(nèi)容加載的類(lèi),在 Android API 23 時(shí)被加入到 support.v7 當(dāng)中。不過(guò)好像很多人對(duì)它還并不了解,網(wǎng)上也沒(méi)有太多相關(guān)的資料。今天這里就來(lái)介紹下 AsyncListUtil 的用法。
首先,AsyncListUtil 通常和 RecyclerView 搭配使用的。其能夠在后臺(tái)線程中加載 Cursor 數(shù)據(jù),同時(shí)保持 UI 和緩存的同步來(lái)實(shí)現(xiàn)更好的用戶體驗(yàn)。不過(guò) AsyncListUtil 是通過(guò)單個(gè)線程加載數(shù)據(jù),因此適用于從二級(jí)存儲(chǔ)(比如硬盤(pán))中加載數(shù)據(jù),而不適用于從網(wǎng)絡(luò)加載數(shù)據(jù)的情況。
RecyclerView 的結(jié)構(gòu)

相信絕大部分 Android 開(kāi)發(fā)者對(duì)此都已經(jīng)非常熟悉了。
RecyclerView + AsyncListUtil 的結(jié)構(gòu)

可以看到 AsyncListUtil 是通過(guò) AsyncListUtil.ViewCallback 來(lái)判斷當(dāng)前數(shù)據(jù)可見(jiàn)的范圍,再通過(guò) AsyncListUtil.DataCallback 從后臺(tái)加載所需的數(shù)據(jù),并在加載完成時(shí)通知 AsyncListUtil.ViewCallback。
因此要使用 AsyncListUtil,首先需要繼承實(shí)現(xiàn) AsyncListUtil.DataCallback 和 AsyncListUtil.ViewCallback 這兩個(gè)抽象類(lèi)。
下面我們通過(guò)代碼來(lái)看看實(shí)際要怎樣實(shí)現(xiàn)?先上效果圖:

數(shù)據(jù)
作者實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 python 腳本 生成了 100,000 條數(shù)據(jù)并存放在 SQLite 數(shù)據(jù)庫(kù)中。每一條數(shù)據(jù)都有 id, title 和 content 三個(gè)屬性。其中的 title 和 content 都是通過(guò) DWYL’s english-words repository 隨機(jī)生成。
ItemSource
class Item(var title: String, var content: String)
interface ItemSource {
fun getCount(): Int
fun getItem(position: Int): Item
fun close()
}
定義 SQLiteItemSource 來(lái)從 SQLite 中獲取數(shù)據(jù):
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
為了創(chuàng)建 AsyncListUtil,我們需要傳入 DataCallback 和 ViewCallback。
首先讓我們實(shí)現(xiàn) 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 是用來(lái)為 AsyncListUtil 提供數(shù)據(jù)訪問(wèn),其中所有方法都會(huì)在后臺(tái)線程中調(diào)用。
其中有兩個(gè)方法必需要實(shí)現(xiàn):
- fillData(data, startPosition, itemCount) - 當(dāng) AsyncListUtil 需要更多數(shù)據(jù)時(shí),將會(huì)在后臺(tái)線程調(diào)用該方法。
- refreshData() - 返回刷新后的數(shù)據(jù)個(gè)數(shù)。
再實(shí)現(xiàn) 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 通過(guò) ViewCallback 主要是做兩件事:
- 通知視圖數(shù)據(jù)已經(jīng)更新(onDataRefresh);
- 了解當(dāng)前視圖所展示數(shù)據(jù)的位置,從而確定什么時(shí)候獲取更多數(shù)據(jù)或釋放掉目前不在窗口內(nèi)的舊數(shù)據(jù)(getItemRangeInto)。
接下來(lái)實(shí)現(xiàn) ScrollListener 來(lái)調(diào)用 AsyncListUtil 的 onRangeChanged() 方法:
private class ScrollListener(val listUtil: AsyncListUtil<in Item>) : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) {
listUtil.onRangeChanged()
}
}
Adapter
至此,AsyncListUtil 所需要的組件都準(zhǔn)備好了,可以來(lái)實(shí)現(xiàn)我們的 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))
}
}
其中實(shí)例化 AsyncListUtil 時(shí)的 500 表示分頁(yè)大小。
要注意的一點(diǎn)是 listUtil.getItem(position) 在指定 position 對(duì)應(yīng)的數(shù)據(jù)仍在被加載時(shí)會(huì)返回 null ,因此需要在 ViewHolder 中處理當(dāng) 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"
}
}
這里當(dāng) item 為 null 時(shí),就簡(jiǎn)單的顯示 "loading"。
最后,在 Activity 中把所有的這些組合起來(lái):
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)
}
}
完整項(xiàng)目代碼可以在 Github 上找到:jasonwyatt/AsyncListUtil-Example。
原文:how-to-use-asynclistutil
延伸閱讀:
理解 Android 新的依賴(lài)方式
RecyclerView 實(shí)現(xiàn)快速滾動(dòng)
現(xiàn)代 Android 開(kāi)發(fā)資源匯總