Android开发程序员Android知识

使用 AsyncListUtil 优化 RecyclerView

2017-09-25  本文已影响247人  极小光

简评: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)

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,我们需要传入 DataCallbackViewCallback

首先让我们实现 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 提供数据访问,其中所有方法都会在后台线程中调用。

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

再实现 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 主要是做两件事:

接下来实现 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 开发资源汇总

上一篇 下一篇

猜你喜欢

热点阅读