使用AsyncListUtil优化RecyclerView

大家好,今天为大家推荐的是来自极光推送的Hevin同学所翻译的文章,重点是介绍AsyncListUtil这个类的使用,它在Android API 23开始就被加入到support.v7当中了,你可能很少看到有人提起过它,但实际上却是非常实用的工具类,至于怎么使用,看完本文你就知道。当然除了AsyncListUtil之外,DiffUtil这个类也是值得你了解一下的,本文虽然没有提及,有兴趣的读者可以自行拓展。文章的示例代码虽是用Kotlin编写,但不妨碍Java的同学阅读。

  • 原文地址:https://zhuanlan.zhihu.com/p/29491805


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 上找到:https://github.com/jasonwyatt/AsyncListUtil-Example

原文链接:https://android.jlelse.eu/how-to-use-asynclistutil-16b5175bb468

微信文章不支持文中的外链,可以跳转到原文继续阅读。

转自:https://mp.weixin.qq.com/s/j-6LxVZgB9t6S87WkiZkRA

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值