You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView中Retrofit2下载时Circular ProgressBar无法实时更新

解决Retrofit2下载时RecyclerView中CircularProgressBar无法实时更新的问题

嘿,我来帮你分析下问题所在,你遇到的情况其实是两个核心问题叠加导致的:主线程被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();
        }
    });
}

最后验证一下

做完这些修改后,你会发现:

  1. 文件写入不再阻塞主线程,UI能实时响应;
  2. 进度更新通过数据模型+Adapter通知,不会因为ViewHolder复用而失效;
  3. 多任务下载的取消操作也不会互相干扰。

这样应该就能解决你的进度条不实时更新的问题了。

内容的提问来源于stack exchange,提问作者Kousalya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 22:23:00