RecyclerView中Retrofit2下载时Circular ProgressBar无法实时更新
嘿,我来帮你分析下问题所在,你遇到的情况其实是两个核心问题叠加导致的:主线程被IO操作阻塞,加上RecyclerView的ViewHolder复用导致View引用失效。咱们一步步来解决:
一、先解决最致命的问题:主线程阻塞
你现在把文件写入的writeResponseBodyToDisk方法直接放在了Retrofit的onResponse回调里,但Retrofit的enqueue回调默认是在主线程执行的!这个while循环的IO操作会直接卡死主线程,导致所有UI更新(包括进度条设置)都被阻塞,直到整个文件下载完成后才会一次性刷新,这就是为什么你只能看到最后100%的原因。
修复方式:把文件写入移到子线程
修改onResponse的代码,把耗时的IO操作丢到子线程里,完成后再回到主线程处理UI:
@Override public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) { // 开启子线程执行文件写入,避免阻塞主线程 new Thread(() -> { boolean downloadResult = writeResponseBodyToDisk(response.body(), vmFileSize, filePath, position); // 写入完成后切回主线程处理后续逻辑 ((Activity) context).runOnUiThread(() -> { if (downloadResult) { Toast.makeText(context, "File downloaded", Toast.LENGTH_SHORT).show(); updateVoiceMailFilePath(position, filePath); } else { deleteVoiceMailFileFromLocalSystem(filePath); } updateViews(position); }); }).start(); }
二、解决ViewHolder复用导致的View引用失效
你直接把circularProgressBar保存为成员变量,但RecyclerView会频繁复用ViewHolder,这个引用很快就会指向其他item的ProgressBar,导致你更新的根本不是当前正在下载的那个item的UI。正确的做法是通过数据驱动UI更新:
1. 在数据模型中添加进度和下载状态字段
假设你的列表项数据模型是VoiceMailItem,添加两个字段记录下载状态:
public class VoiceMailItem { // 原有字段:otpapi、userName、vmFileName... private int downloadProgress; // 当前下载进度 private boolean isDownloading; // 是否正在下载 // Getter & Setter public int getDownloadProgress() { return downloadProgress; } public void setDownloadProgress(int progress) { this.downloadProgress = progress; } public boolean isDownloading() { return isDownloading; } public void setDownloading(boolean downloading) { isDownloading = downloading; } }
2. 在Adapter的ViewHolder中绑定数据到UI
ViewHolder的bind方法里根据数据模型的状态更新ProgressBar:
public class VoiceMailViewHolder extends RecyclerView.ViewHolder { CircularProgressBar circularProgressBar; public VoiceMailViewHolder(View itemView) { super(itemView); circularProgressBar = itemView.findViewById(R.id.your_progress_bar_id); } public void bind(VoiceMailItem item) { if (item.isDownloading()) { circularProgressBar.setIndeterminate(false); circularProgressBar.setProgress(item.getDownloadProgress()); } else { // 非下载状态下的UI处理,比如设置进度为100%或隐藏 circularProgressBar.setProgress(item.getDownloadProgress()); } } }
3. 修改进度更新逻辑,通过Adapter通知UI刷新
更新writeResponseBodyToDisk方法,每次计算出进度后,切回主线程更新数据模型,再调用notifyItemChanged触发对应item的UI更新:
// 给方法增加position参数,用来定位要更新的item private boolean writeResponseBodyToDisk(ResponseBody body, String fileSize, String filePath, int position) { try { InputStream inputStream = null; OutputStream outputStream = null; try { byte[] fileReader = new byte[8192]; long fileSizeDownloaded = 0; long lengthOfFile = Long.parseLong(String.format("%.0f", Double.parseDouble(fileSize))) * 1024; inputStream = body.byteStream(); outputStream = new FileOutputStream(filePath); while (true) { int read = inputStream.read(fileReader); // 这里改用针对position的取消标记,避免全局变量干扰多任务 if (cancelDownloadFlags.get(position, false)) { inputStream.close(); ((Activity) context).runOnUiThread(() -> { VoiceMailItem item = adapter.getItem(position); if (item != null) { item.setDownloading(false); adapter.notifyItemChanged(position); } cancelDownloadFlags.delete(position); // 重置取消标记 }); return false; } if (read == -1) break; outputStream.write(fileReader, 0, read); fileSizeDownloaded += read; if (lengthOfFile > 0) { int progress = (int) (fileSizeDownloaded * 100 / lengthOfFile); // 切回主线程更新数据并通知UI ((Activity) context).runOnUiThread(() -> { VoiceMailItem item = adapter.getItem(position); if (item != null) { item.setDownloadProgress(progress); item.setDownloading(true); adapter.notifyItemChanged(position); } }); } } outputStream.flush(); // 下载完成,更新最终状态 ((Activity) context).runOnUiThread(() -> { VoiceMailItem item = adapter.getItem(position); if (item != null) { item.setDownloadProgress(100); item.setDownloading(false); adapter.notifyItemChanged(position); } }); return true; } catch (IOException e) { ((Activity) context).runOnUiThread(() -> { VoiceMailItem item = adapter.getItem(position); if (item != null) { item.setDownloading(false); adapter.notifyItemChanged(position); } }); return false; } finally { if (inputStream != null) inputStream.close(); if (outputStream != null) outputStream.close(); } } catch (IOException e) { ((Activity) context).runOnUiThread(() -> { VoiceMailItem item = adapter.getItem(position); if (item != null) { item.setDownloading(false); adapter.notifyItemChanged(position); } }); return false; } }
三、修复取消下载的逻辑
原来的全局cancelDownload布尔值会导致多个下载任务互相干扰,改用SparseBooleanArray来记录每个position的取消状态:
// 在Activity/Fragment中定义 private SparseBooleanArray cancelDownloadFlags = new SparseBooleanArray(); // 把取消下载的点击事件移到ViewHolder的bind方法里 public void bind(VoiceMailItem item, int position) { // 原有bind逻辑... circularProgressBar.setOnClickListener(v -> { if (item.isDownloading()) { cancelDownloadFlags.put(position, true); Toast.makeText(itemView.getContext(), "cancel clicked", Toast.LENGTH_LONG).show(); } }); }
最后验证一下
做完这些修改后,你会发现:
- 文件写入不再阻塞主线程,UI能实时响应;
- 进度更新通过数据模型+Adapter通知,不会因为ViewHolder复用而失效;
- 多任务下载的取消操作也不会互相干扰。
这样应该就能解决你的进度条不实时更新的问题了。
内容的提问来源于stack exchange,提问作者Kousalya

