离开Activity后如何获取正确的RecyclerView ViewHolder
解决FirebaseRecyclerAdapter下载进度在Activity返回后无法正确更新视图的问题
这个问题的核心原因其实很好理解:RecyclerView的ViewHolder是复用机制,当你离开Activity再返回时,原来绑定下载任务的ViewHolder可能已经被回收,转而绑定了其他列表项;同时异步的下载回调还拿着旧的ViewHolder引用,自然会更新错误的视图。要解决这个问题,我们需要把下载状态和进度与数据模型绑定,而不是依赖ViewHolder,同时用ViewModel来管理跨生命周期的下载任务。下面是具体的实现方案:
1. 给数据模型添加下载状态字段
首先,把下载相关的状态(是否正在下载、当前进度)直接附加到你的列表数据类中,这样不管ViewHolder怎么复用,数据本身的状态是准确的。比如:
data class FileModel( val storageRef: StorageReference? = null, val fileName: String? = null, // 新增本地状态字段,不需要同步到Firebase数据库 var isDownloading: Boolean = false, var downloadProgress: Int = 0 )
2. 用ViewModel管理下载任务与状态
ViewModel的生命周期和Activity/Fragment绑定,不会因为页面离开、重建而丢失数据。我们可以用它来保存正在进行的下载任务,以及维护数据模型的状态:
class DownloadViewModel : ViewModel() { // 用文件路径作为键,保存正在进行的下载任务 val activeDownloads = mutableMapOf<String, DownloadTask>() // 如果你的数据源是本地维护的列表,也可以放在这里;如果是Firebase实时数据库,直接关联Adapter的数据源即可 }
3. 在Adapter中根据数据状态更新视图
修改FirebaseRecyclerAdapter的onBindViewHolder方法,每次绑定数据时,根据FileModel的状态来更新UI,而不是依赖ViewHolder的缓存引用:
override fun onBindViewHolder(holder: FileViewHolder, position: Int, model: FileModel) { // 绑定基础数据(比如文件名) holder.fileNameTv.text = model.fileName // 根据下载状态切换视图 if (model.isDownloading) { holder.downloadBtn.visibility = View.GONE holder.progressBar.visibility = View.VISIBLE holder.progressBar.progress = model.downloadProgress } else { holder.downloadBtn.visibility = View.VISIBLE holder.progressBar.visibility = View.GONE } // 下载按钮点击事件 holder.downloadBtn.setOnClickListener { startDownload(model, holder.adapterPosition) } }
4. 启动下载时更新数据状态并绑定回调
启动下载任务时,更新对应数据模型的状态,同时在进度回调里更新数据并通知Adapter刷新指定位置:
private fun startDownload(model: FileModel, position: Int) { val storageRef = model.storageRef ?: return val localFile = File(getExternalFilesDir(null), model.fileName) // 更新数据状态 model.isDownloading = true notifyItemChanged(position) // 启动下载任务并保存到ViewModel val downloadTask = storageRef.getFile(localFile) viewModel.activeDownloads[storageRef.path] = downloadTask // 进度回调:更新数据模型并刷新UI downloadTask.addOnProgressListener { snapshot -> val progress = (100.0 * snapshot.bytesTransferred / snapshot.totalByteCount).toInt() model.downloadProgress = progress notifyItemChanged(position) }.addOnCompleteListener { task -> model.isDownloading = false notifyItemChanged(position) viewModel.activeDownloads.remove(storageRef.path) if (task.isSuccessful) { // 下载完成的逻辑(比如提示用户) } else { // 下载失败的逻辑 } } }
5. 处理Activity返回后的状态恢复
在Activity的onResume方法中,让ViewModel重新绑定所有活跃下载任务的回调,确保返回后进度能继续更新到正确的视图:
override fun onResume() { super.onResume() viewModel.activeDownloads.forEach { (path, task) -> // 找到对应的FileModel val model = adapter.currentList.find { it.storageRef?.path == path } ?: return@forEach // 重新绑定进度回调 task.addOnProgressListener { snapshot -> val progress = (100.0 * snapshot.bytesTransferred / snapshot.totalByteCount).toInt() model.downloadProgress = progress // 找到当前模型在列表中的位置并刷新 val position = adapter.currentList.indexOf(model) if (position != -1) { adapter.notifyItemChanged(position) } }.addOnCompleteListener { model.isDownloading = false val position = adapter.currentList.indexOf(model) if (position != -1) { adapter.notifyItemChanged(position) } viewModel.activeDownloads.remove(path) } } }
关键注意事项
- 永远不要直接持有ViewHolder的引用:ViewHolder会被RecyclerView复用,持有引用会导致更新错误的视图,甚至内存泄漏。
- 避免将本地下载进度同步到Firebase数据库:除非你需要所有客户端都看到这个进度,否则本地维护状态即可。
- 记得取消无用的下载任务:当列表项被滑出屏幕或者Activity销毁时,取消对应的下载任务,避免浪费资源。
内容的提问来源于stack exchange,提问作者Jean-Pascal MEWENEMESSE
相关产品推荐
相关产品推荐

