使用 AsyncListUtil 優(yōu)化 RecyclerView

簡(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.DataCallbackAsyncListUtil.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,我們需要傳入 DataCallbackViewCallback

首先讓我們實(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ā)資源匯總

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,733評(píng)論 25 709
  • 度娘是這樣解釋的:瑜伽是一項(xiàng)有著5000年歷史的關(guān)于身體、心理以及精神的練習(xí),起源于印度,其目的是改善人的身體和心...
    O_oo噠噠喵閱讀 606評(píng)論 0 0
  • 花團(tuán)開(kāi)滿樹(shù), 花帶鋪滿路, 滿城花之海, 滿眼花之圖。
    珠江潮平閱讀 218評(píng)論 11 16
  • 觀月 吳剛倚桂樹(shù),嫦娥攬玉兔。 廣寒宮內(nèi)暖?對(duì)視回眸處
    花瓣旁的小葉子閱讀 253評(píng)論 0 0
  • 你今年23歲,身份證上面或許還大1歲。這一年六月份,你剛從學(xué)校走出來(lái)??粗饷嫖宀拾邤痰氖澜纾阋彩浅錆M斗志和希...
    偽文字青年閱讀 274評(píng)論 0 0

友情鏈接更多精彩內(nèi)容