使用 AsyncListUtil 優化 RecyclerView

簡評: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.DataCallbackAsyncListUtil.ViewCallback 這兩個抽象類。

下面我們通過代碼來看看實際要怎樣實現?先上效果圖:

數據

作者實現了一個簡單的 python 腳本 生成了 100,000 條數據並存放在 SQLite 資料庫中。每一條數據都有 id, title 和 content 三個屬性。其中的 title 和 content 都是通過 DWYL』s english-words repository 隨機生成。

ItemSource

class Item(var title: String, var content: String)nninterface ItemSource {n fun getCount(): Intn fun getItem(position: Int): Itemn fun close()n}n

定義 SQLiteItemSource 來從 SQLite 中獲取數據:

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

Callbacks

為了創建 AsyncListUtil,我們需要傳入 DataCallback 和 ViewCallback。

首先讓我們實現 DataCallback:

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

DataCallback 是用來為 AsyncListUtil 提供數據訪問,其中所有方法都會在後台線程中調用。

其中有兩個方法必需要實現:

  • fillData(data, startPosition, itemCount) - 當 AsyncListUtil 需要更多數據時,將會在後台線程調用該方法。
  • refreshData() - 返回刷新後的數據個數。

再實現 ViewCallback:

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

AsyncListUtil 通過 ViewCallback 主要是做兩件事:

  1. 通知視圖數據已經更新(onDataRefresh);
  2. 了解當前視圖所展示數據的位置,從而確定什麼時候獲取更多數據或釋放掉目前不在窗口內的舊數據(getItemRangeInto)。

接下來實現 ScrollListener 來調用 AsyncListUtil 的 onRangeChanged() 方法:

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

Adapter

至此,AsyncListUtil 所需要的組件都準備好了,可以來實現我們的 RecyclerView.Adapter 了:

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

其中實例化 AsyncListUtil 時的 500 表示分頁大小。

要注意的一點是 listUtil.getItem(position) 在指定 position 對應的數據仍在被載入時會返回 null ,因此需要在 ViewHolder 中處理當 item 為 null 的情況:

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

這裡當 item 為 null 時,就簡單的顯示 "loading"。

最後,在 Activity 中把所有的這些組合起來:

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

完整項目代碼可以在 Github 上找到:jasonwyatt/AsyncListUtil-Example。

原文:how-to-use-asynclistutil

日報擴展閱讀:

  • 現代 Android 開發資源匯總

推薦閱讀:

《奧日與黑暗森林》這樣的遊戲主要需要哪些技術,幾個人的小團隊能實現嗎?
[譯] 閱讀 NodeJS 文檔,我學到了這 19 件事情
【偽教程】手把手教你成為matlab作曲家
我從編程總結的 22 個經驗

TAG:Android | Android开发 | 编程 |