Retrofit文件上传和文件下载


项目中使用了Retrofit2 网络框架,对Retrofit的文件上传和下载进行记录。

文件上传


文件上传 一般采用POST 的方式,并使用@Multipart 声明为多部分,可同时上传文本和文件,接口设置如下:

interface UploadService {
    @Multipart
    @POST("uploadImg")
    Observable<BaseResult<String>> upload(
            @Part List<MultipartBody.Part> partList);
}

step1 创建UploadService

OkHttpClient httpClient = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)  //连接超时
        .readTimeout(10, TimeUnit.SECONDS)   //读取超时
        .build();

Retrofit retrofit = new Retrofit.Builder().baseUrl(Contacts.BASE_URL)  //配置Retrofit 端
        .addConverterFactory(FastJsonConvertFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .client(httpClient)
        .build();

UploadService uploadService = retrofit.create(UploadService.class);

step2 构建文件RequestBody

// 创建 RequestBody,用于封装 请求RequestBody
File imageFile = new File(filePath);  //上传文件对象
RequestBody requestFile =
        RequestBody.create(guessMimeType(imageFile.getName()), imageFile);  //这里使用了工具类,获取文件类型

guseeMimeType方法为:

private static MediaType guessMimeType(String path) {
    FileNameMap fileNameMap = URLConnection.getFileNameMap();
    path = path.replace("#", "");   //解决文件名中含有#号异常的问题
    String contentType = fileNameMap.getContentTypeFor(path);
    if (contentType == null) {
        contentType = "application/octet-stream";
    }
    return MediaType.parse(contentType);
}

step3 构建MultipartBody 进行上传

MultipartBody.Builder builder = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)//表单类型 "multipart/form-data"    
        .addFormDataPart("version", Contacts.version)   //   项目的接口公共参数
        .addFormDataPart("data", data_json)   //json 字符串
        .addFormDataPart("sign", sign)
        .addFormDataPart("file", imageFile.getName(), requestFile);   //上传的文件

List<MultipartBody.Part> parts = builder.build().parts();
//进行上传
uploadService.upload(parts)
                .compose(RxSchedulerHepler.<BaseResult<String>>io_main());   //线程切换

文件下载


文件下载相较于 文件上传复杂一点,其中包含了 ResponseBody 流的读取,文件的写入,下载进度的更新

之前进度之类的更新,采用的是接口回调的形式进行更新,自从RxJava的出现改变了这样的方式,采用观察者的订阅方式进行进度的更新。

下载接口的设计如下:

@GET
@Streaming    //使用Streaming 方式 Retrofit 不会一次性将ResponseBody 读取进入内存,否则文件很多容易OOM
Flowable<ResponseBody> download2(@Url String url);  //返回值使用 ResponseBody 之后会对ResponseBody 进行读取

Step 1 创建ApiService (so easy!)

OkHttpClient httpClient = new OkHttpClient.Builder()
        .build();


Retrofit retrofit = new Retrofit.Builder().baseUrl(Contacts.BASE_URL)
        .addConverterFactory(FastJsonConvertFactory.create())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .client(httpClient)
        .build();

ApiService apiService = retrofit.create(ApiService.class);

Step2 获取ResponseBody 进行文件流的读写

apiService.download2(url)
        .flatMap(new Function<ResponseBody, Publisher<Boolean>>() {
            @Override
            public Publisher<Boolean> apply(@NonNull final ResponseBody responseBody) throws Exception {
                return Flowable.create(new FlowableOnSubscribe<Boolean>() {
                    @Override
                    public void subscribe(FlowableEmitter<Boolean> e) throws Exception {
                    
                        File saveFile = new File(filePath, fileName);
                        InputStream inputStream = null;
                        OutputStream outputStream = null;
                        try {
                            try {
                                int readLen;
                            
                                byte[] buffer = new byte[1024];

                                inputStream = responseBody.byteStream();
                                outputStream = new FileOutputStream(saveFile);

                          
                                while ((readLen = inputStream.read(buffer)) != -1 && !e.isCancelled()) {
                                    outputStream.write(buffer, 0, readLen);
                                }
                                outputStream.flush(); // This is important!!!
                                e.onComplete();
                            } finally {
                                closeQuietly(inputStream);
                                closeQuietly(outputStream);
                                closeQuietly(responseBody);
                            }
                        } catch (Exception exception) {
                            e.onError(exception);
                        }
                    }
                }, BackpressureStrategy.LATEST);
            }
        })

其中包含了一个关闭资源方法:

public static void closeQuietly(Closeable closeable) {
    if (closeable != null) {
        try {
            closeable.close();
        } catch (RuntimeException rethrown) {
            throw rethrown;
        } catch (Exception ignored) {
        }
    }
}

方法分析:

采用RxJava2 的Flowable ,Flowable 在Observable 的基础上进行了背压的处理

接着获取到ResponseBody,使用RxJava的flatMap 对数据源进行变换,利用FlowableEmitter可以将下载成功的消息发射出去

接着就是熟悉的文件读写操作文件读写完成之后,发射 成功的信息

Step 3 下载进度

Step 2中完成了文件的读写,但是缺少了挺重要的进度回调操作,对Step 2 的代码进行改进

apiService.download2(url)
        .flatMap(new Function<ResponseBody, Publisher<DownloadStatus>>() {
            @Override
            public Publisher<DownloadStatus> apply(@NonNull final ResponseBody responseBody) throws Exception {
                return Flowable.create(new FlowableOnSubscribe<DownloadStatus>() {
                    @Override
                    public void subscribe(FlowableEmitter<DownloadStatus> e) throws Exception {
                        //创建文件
                        File saveFile = new File(filePath, fileName);

                        InputStream inputStream = null;
                        OutputStream outputStream = null;
                        try {
                            try {
                                int readLen;
                                int downloadSize = 0;
                                byte[] buffer = new byte[8192];

                                DownloadStatus status = new DownloadStatus();
                                inputStream = responseBody.byteStream();  //获取 输入流
                                outputStream = new FileOutputStream(saveFile);  //文件的输出流

                                long contentLength = responseBody.contentLength();  //文件的总长度

                                status.setTotalSize(contentLength);   

                                while ((readLen = inputStream.read(buffer)) != -1 && !e.isCancelled()) {
                                    outputStream.write(buffer, 0, readLen);
                                    downloadSize += readLen;  
                                    status.setDownloadSize(downloadSize);
                                    e.onNext(status);  //读取完一段就将下载进度发射出去
                                }

                                outputStream.flush(); // This is important!!!
                                e.onComplete();   //发射下载完成的信息
                            } finally {
                                closeQuietly(inputStream);
                                closeQuietly(outputStream);
                                closeQuietly(responseBody);
                            }
                        } catch (Exception exception) {

                        }
                    }
                }, BackpressureStrategy.LATEST);
            }
        })
        .toObservable()  //转换成我们熟悉的Observable 
        .debounce(200, TimeUnit.MICROSECONDS)  //进行200秒的过滤操作
        .compose(RxSchedulerHepler.<DownloadStatus>io_main());  //线程切换

DownLoadStatus对下载的状态进行了包装,包含了下载长度,总长度 字段

public class DownloadStatus implements Parcelable {
    public static final Creator<DownloadStatus> CREATOR
            = new Creator<DownloadStatus>() {
        @Override
        public DownloadStatus createFromParcel(Parcel source) {
            return new DownloadStatus(source);
        }

        @Override
        public DownloadStatus[] newArray(int size) {
            return new DownloadStatus[size];
        }
    };


    private long totalSize;
    private long downloadSize;

    public DownloadStatus() {
    }

    public DownloadStatus(long downloadSize, long totalSize) {
        this.downloadSize = downloadSize;
        this.totalSize = totalSize;
    }

    public DownloadStatus(boolean isChunked, long downloadSize, long totalSize) {

        this.downloadSize = downloadSize;
        this.totalSize = totalSize;
    }

    protected DownloadStatus(Parcel in) {

        this.totalSize = in.readLong();
        this.downloadSize = in.readLong();
    }

    public long getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(long totalSize) {
        this.totalSize = totalSize;
    }

    public long getDownloadSize() {
        return downloadSize;
    }

    public void setDownloadSize(long downloadSize) {
        this.downloadSize = downloadSize;
    }

    /**
     * 获得格式化的总Size
     *
     * @return example: 2KB , 10MB
     */
    public String getFormatTotalSize() {
        return formatSize(totalSize);
    }

    public String getFormatDownloadSize() {
        return formatSize(downloadSize);
    }

    public static String formatSize(long size) {
        String hrSize;
        double b = size;
        double k = size / 1024.0;
        double m = ((size / 1024.0) / 1024.0);
        double g = (((size / 1024.0) / 1024.0) / 1024.0);
        double t = ((((size / 1024.0) / 1024.0) / 1024.0) / 1024.0);
        DecimalFormat dec = new DecimalFormat("0.00");
        if (t > 1) {
            hrSize = dec.format(t).concat(" TB");
        } else if (g > 1) {
            hrSize = dec.format(g).concat(" GB");
        } else if (m > 1) {
            hrSize = dec.format(m).concat(" MB");
        } else if (k > 1) {
            hrSize = dec.format(k).concat(" KB");
        } else {
            hrSize = dec.format(b).concat(" B");
        }
        return hrSize;
    }


    /**
     * 获得格式化的状态字符串
     *
     * @return example: 2MB/36MB
     */
    public String getFormatStatusString() {
        return getFormatDownloadSize() + "/" + getFormatTotalSize();
    }

    /**
     * 获得下载的百分比, 保留两位小数
     *
     * @return example: 5.25%
     */
    public String getPercent() {
        String percent;
        Double result;
        if (totalSize == 0L) {
            result = 0.0;
        } else {
            result = downloadSize * 1.0 / totalSize;
        }
        NumberFormat nf = NumberFormat.getPercentInstance();
        nf.setMinimumFractionDigits(2);//控制保留小数点后几位,2:表示保留2位小数点
        percent = nf.format(result);
        return percent;
    }

    /**
     * 获得下载的百分比数值
     *
     * @return example: 5%  will return 5, 10% will return 10.
     */
    public long getPercentNumber() {
        double result;
        if (totalSize == 0L) {
            result = 0.0;
        } else {
            result = downloadSize * 1.0 / totalSize;
        }
        return (long) (result * 100);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(this.totalSize);
        dest.writeLong(this.downloadSize);
    }
}

Step 4在Activity中进行文件下载测试

String url = "http://www.bz55.com/uploads/allimg/150701/140-150F1142638.jpg";
Disposable    mSubscribe = DownLoadUtil3.download(url, Environment.getExternalStorageDirectory().getAbsolutePath(), "140-150F1142638.jpg")
                .subscribe(new Consumer<DownloadStatus>() {
                    @Override
                    public void accept(@NonNull DownloadStatus downloadStatus) throws Exception {
                        long downloadSize = downloadStatus.getDownloadSize();
                        long totalSize = downloadStatus.getTotalSize();
                        Log.e("TAG", "onNext  -->" + downloadSize + "------" + totalSize);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {
                        Log.e("TAG", "onError:--->" + throwable);
                    }
                }, new Action() {
                    @Override
                    public void run() throws Exception {
                        Log.e("TAG", "下载完成");
                    }
                });
    //停止下载的方式为 mSubscribe.dispose();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,400评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 我从去年开始使用 RxJava ,到现在一年多了。今年加入了 Flipboard 后,看到 Flipboard 的...
    Jason_andy阅读 5,451评论 7 62
  • 本文包括:1、文件上传概述2、利用 Commons-fileupload 组件实现文件上传3、核心API——Dis...
    廖少少阅读 12,509评论 5 91
  • 最近常常看到简书上关于写作的文章,好多人把写作当作一种习惯来培养。内容是什么不是重点,坚持每天书写才是关键。 我突...
    HolyCow阅读 295评论 4 1