29、RecyclerView 与协程:在列表加载中使用 Kotlin 协程
说实话,RecyclerView 本身并不难。难的是数据加载。
你想想看,一个列表要展示数据,总得从网络或数据库拿吧?拿数据就得做异步操作。以前我们用回调、用 AsyncTask,代码写得跟千层饼似的。后来有了协程,嗯,这才像人写的代码。
我个人习惯,只要涉及 RecyclerView 的数据加载,一律上协程。为什么?因为协程能让你用同步的写法,写出异步的代码。说白了,就是省心。
为什么 RecyclerView 需要协程?
我们先看一个典型场景:用户打开一个列表页,我们需要从 API 拉取数据,然后更新 UI。
在 Android 里,网络请求不能在主线程执行。那怎么办?以前的做法是开一个线程,请求完数据,再切回主线程更新适配器。代码大概长这样:
// 老式写法
Thread {
val data = api.fetchData() // 网络请求
runOnUiThread {
adapter.submitList(data)
}
}.start()
这代码有什么问题?问题大了。线程管理麻烦、生命周期没处理、容易内存泄漏。我在项目中遇到过好几次,Activity 销毁了,线程还在跑,结果更新 UI 时直接崩溃。
协程就是来解决这些问题的。它轻量、可控、还能自动取消。
协程在 RecyclerView 中的基本用法
我们先从最简单的开始。假设你有一个 ViewModel,里面用协程加载数据:
class MyViewModel : ViewModel() {
private val _items = MutableLiveData<List<String>>()
val items: LiveData<List<String>> = _items
fun loadData() {
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
// 模拟网络请求
fetchDataFromApi()
}
_items.value = result
}
}
}
然后在 Activity 或 Fragment 里观察 LiveData,拿到数据后提交给适配器:
viewModel.items.observe(this) { data ->
adapter.submitList(data)
}
嗯,这样写已经比回调好太多了。但还不够。为什么?因为这里有个细节:数据加载过程中,用户可能已经离开了页面。
结合 Lifecycle 安全加载
我个人建议,在 Fragment 里用 lifecycleScope 来启动协程。这样协程的生命周期就和 Fragment 绑定在一起了。
class MyFragment : Fragment() {
private lateinit var adapter: MyAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = MyAdapter()
recyclerView.adapter = adapter
// 使用 lifecycleScope
lifecycleScope.launch {
val data = withContext(Dispatchers.IO) {
fetchData()
}
adapter.submitList(data)
}
}
}
这样写的好处是:Fragment 销毁时,协程自动取消。不会出现「页面没了,请求还在跑」的尴尬情况。
分页加载与协程
列表加载最头疼的是什么?分页。
用户往下滑,你要加载更多。加载过程中不能卡 UI,还要防止重复请求。我以前用回调写过分页,那代码简直不忍直视。
用协程就好多了。我们可以在适配器的 onBindViewHolder 里触发加载,或者用 addOnScrollListener 监听滚动位置。
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val totalItemCount = layoutManager.itemCount
// 滑到底部时加载更多
if (lastVisibleItem >= totalItemCount - 2) {
loadMore()
}
}
})
private fun loadMore() {
lifecycleScope.launch {
val moreData = withContext(Dispatchers.IO) {
api.fetchPage(currentPage + 1)
}
currentPage++
adapter.addItems(moreData)
}
}
这里要注意一个坑:防止重复触发。用户快速滑动时,onScrolled 可能会被多次调用。我建议加一个标志位:
private var isLoading = false
private fun loadMore() {
if (isLoading) return
isLoading = true
lifecycleScope.launch {
try {
val moreData = withContext(Dispatchers.IO) {
api.fetchPage(currentPage + 1)
}
currentPage++
adapter.addItems(moreData)
} finally {
isLoading = false
}
}
}
嗯,这样就不会重复请求了。
协程与 ListAdapter 的配合
ListAdapter 本身已经帮我们处理了 diff 计算。但数据加载的时机,还是得我们自己控制。
我常用的模式是:在 ViewModel 里用 StateFlow 暴露数据,Fragment 里收集流,然后提交给适配器。
// ViewModel
class MyViewModel : ViewModel() {
private val _state = MutableStateFlow<List<String>>(emptyList())
val state: StateFlow<List<String>> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
val data = withContext(Dispatchers.IO) {
fetchData()
}
_state.value = data
}
}
}
// Fragment
lifecycleScope.launch {
viewModel.state.collect { data ->
adapter.submitList(data)
}
}
这样写的好处是:数据变化自动通知 UI,不需要手动调用 observe。而且 StateFlow 支持协程取消,生命周期安全。
错误处理与加载状态
列表加载不可能永远成功。网络断了、服务器挂了、数据格式变了……这些都要处理。
我习惯用 sealed class 定义加载状态:
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
然后在 ViewModel 里:
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun loadData() {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val data = withContext(Dispatchers.IO) {
fetchData()
}
_state.value = UiState.Success(data)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "未知错误")
}
}
}
Fragment 里根据状态显示不同 UI:加载中显示进度条,成功显示列表,失败显示重试按钮。
避坑指南:协程与 RecyclerView 的常见问题
我总结几个实际项目中踩过的坑:
- 不要在 ViewHolder 里启动协程。ViewHolder 会被复用,协程的生命周期很难管理。协程应该在 Fragment 或 ViewModel 里启动。
- 注意协程的取消。如果用户快速滑动,列表会频繁刷新。旧的协程应该被取消,否则可能造成数据错乱。
- 不要在主线程做大量计算。即使是用协程,
Dispatchers.Main也不适合做耗时操作。网络请求、数据库读写、JSON 解析,统统丢到Dispatchers.IO。
我曾经在一个项目里,把图片解码放在了主线程。结果列表滑动时卡成 PPT。后来改成 withContext(Dispatchers.Default),瞬间流畅了。
知识体系总览
下面这张图总结了 RecyclerView 与协程配合的核心逻辑:
总结
协程和 RecyclerView 的配合,说白了就是三件事:
- 用协程做异步加载,代码简洁
- 用 Lifecycle 管理协程生命周期,避免泄漏
- 用 StateFlow 或 LiveData 桥接数据,自动更新 UI
做到这三点,你的列表加载基本就稳了。剩下的就是根据业务需求,加上分页、错误处理、加载状态这些细节。
核心要点: 协程不是银弹,但它确实让 RecyclerView 的数据加载变得优雅了很多。我个人建议,新项目一律用协程 + Flow 的组合,老项目也尽量迁移过来。你试试看,写起来真的舒服。