Android 实现一个简单的文件上传工具
之前写了一篇关于下载的文章,有需要的可以了解下:Android 实现一个简单的文件下载工具
和下载功能一样,文件上传的功能在开发中也经常用到,所以这次我们同样基于okhttp实现一个简单的文件上传工具。
基本实现原理
首先我们定义一个OkHttpManager类来进行基本的网络请求,这里采用异步的方式并对请求的headers进行配置:
public class OkHttpManager {
private OkHttpClient.Builder builder;
private OkHttpManager() {
builder = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS);
}
public static OkHttpManager getInstance() {
return OkHttpHolder.instance;
}
private static class OkHttpHolder {
private static final OkHttpManager instance = new OkHttpManager();
}
public Call initRequest(String url, RequestBody requestBody, Map<String, String> headers, final Callback callback) {
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.post(requestBody);
if (headers != null && headers.size() > 0) {
Headers.Builder headerBuilder = new Headers.Builder();
for (String key : headers.keySet()) {
headerBuilder.add(key, headers.get(key));
}
requestBuilder.headers(headerBuilder.build());
}
Call call = builder.build().newCall(requestBuilder.build());
call.enqueue(callback);
return call;
}
}
由于我们要做的上传功能分为两类:
1、表单形式上传
2、直接将文件作为请求体上传
所以先定义一个BaseUploadRequest抽象基类,来进行RequestBody的构造,以及用upload()方法发起上传。
public abstract class BaseUploadRequest {
protected String url;
protected Map<String, String> params;
protected Map<String, String> headers;
private Handler handler;
public Call upload(final UploadCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("UploadCallback can not be null");
}
UploadProgressHandler progressHandler = new UploadProgressHandler(callback);
handler = progressHandler.getHandler();
handler.sendEmptyMessage(START);
RequestBody requestBody = initRequestBody();
requestBody = new ProgressRequestBody(requestBody, handler);
return OkHttpManager.getInstance().initRequest(url, requestBody, headers, new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Message message = Message.obtain();
message.what = ERROR;
message.obj = e.toString();
handler.sendMessage(message);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response != null && response.isSuccessful()) {
Message message = Message.obtain();
message.what = FINISH;
message.obj = response.body().string();
handler.sendMessage(message);
}
}
});
}
protected abstract RequestBody initRequestBody();
}
在这里我们也处理了子线程和主线程切换的问题,通过Handler将最终的网络响应从子线程切换到主线程,以便进行UI操作。同时上边有一个ProgressRequestBody类,通过这个类我们可以监听到文件上传的进度,并用Handler将进度发送到主线程,具体的细节可参考源文件。
接下来我们看表单形式上传,首先要继承BaseUploadRequest类,并重写initRequestBody()方法:
public abstract class FormUploadRequest extends BaseUploadRequest {
@Override
protected RequestBody initRequestBody() {
RequestBody requestBody;
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if (params != null && params.size() > 0) {
for (String key : params.keySet()) {
builder.addFormDataPart(key, params.get(key));
}
}
buildRequestBody(builder);
requestBody = builder.build();
return requestBody;
}
protected abstract void buildRequestBody(MultipartBody.Builder builder);
}
主要就是添加请求参数,考虑到要上传的文件可以直接从本地读取、也可以是字节流(例如拍照后裁剪得到bitmap转化成byte等等),所以加了buildRequestBody()抽象方法,以便将不同类型的文件添加到RequestBody中。当要上传本地文件时,定义FileUploadRequest类,重写buildRequestBody()方法:
@Override
protected void buildRequestBody(MultipartBody.Builder builder) {
for (UploadFile file : files) {
RequestBody fileBody = RequestBody.create(MediaType.parse(Utils.getMimeType(file.getName())), file.getFile());
builder.addFormDataPart(file.getName(), file.getFilename(), fileBody);
}
}
当要上传字节流时,定义BytesUploadRequest类,重写buildRequestBody()方法:
@Override
protected void buildRequestBody(MultipartBody.Builder builder) {
type = TextUtils.isEmpty(type) ? "application/octet-stream" : type;
for (UploadByte bytes : byteList) {
RequestBody fileBody = RequestBody.create(MediaType.parse(type), bytes.getBytes());
builder.addFormDataPart(bytes.getName(), bytes.getFilename(), fileBody);
}
}
通过循环我们实现了多文件上传的功能。
到这里表单形式上传的RequestBody就构建完毕了。
直接将文件作为请求体上传情况的RequestBody构建就简单的多了,定义一个DirectUploadRequest类重写BaseUploadRequest的initRequestBody()方法,根据媒体类型直接通过本地文件或字节流得到RequestBody:
@Override
protected RequestBody initRequestBody() {
type = TextUtils.isEmpty(type) ? "application/octet-stream" : type;
if (file != null) {
return RequestBody.create(MediaType.parse(type), file);
}
return RequestBody.create(MediaType.parse(type), bytes);
}
为了方便使用,我们可以通过Builder的形式来配置上传需要的各类参数,这里用表单形式上传的Builder类为例子说明一下:
public class FormUploadBuilder extends BaseUploadBuilder<FormUploadBuilder> {
private List<UploadFile> files = new ArrayList<>();//本地文件集合
private String type;//媒体类型
private List<UploadByte> byteList = new ArrayList<>();//字节流集合
/**
* 添加单个文件
*/
public FormUploadBuilder addFile(String name, String filename, File file) {
files.add(new UploadFile(name, filename, file));
return this;
}
/**
* 添加文件集合
*/
public FormUploadBuilder addFiles(List<UploadFile> files) {
this.files = files;
return this;
}
/**
* 添加单个字节流
*/
public FormUploadBuilder addByte(String name, String filename, byte[] bytes) {
byteList.add(new UploadByte(name, filename, bytes));
return this;
}
/**
* 添加字节流集合
*/
public FormUploadBuilder addBytes(List<UploadByte> byteList) {
this.byteList = byteList;
return this;
}
/**
* 上传字节流的媒体类型
*/
public FormUploadBuilder addType(String type) {
this.type = type;
return this;
}
/**
* 本地文件类型request(表单式)
*/
public FileUploadRequest fileUploadBuild() {
return new FileUploadRequest(url, files, params, headers);
}
/**
* 字节流类型request(表单式)
*/
public BytesUploadRequest bytesUploadBuild() {
return new BytesUploadRequest(url, byteList, type, params, headers);
}
}
直接将文件作为请求体上传的Builder的可参考源码,基本是类似的。
关于回调方法如下:
public interface UploadCallback extends FileCallback {
void onStart();
void onProgress(long currentSize, long totalSize, float progress);
void onFinish(String response);
void onError(String error);
}
其中onFinish()方法的参数代表请求成功的响应串,可转换成你需要的格式的数据,例如json对象。同时所有的回调都在UI线程。
如果只需要某几个回调则可以通过SimpleUploadCallback来实现。
用法示例
表单式上传字节流:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
ByteArrayOutputStream bs = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bs);
DUtil.initFormUpload()
.url(url)
.addParam("key", "value")
.addParam("key1", "value1")
.addByte("file", "BeautyImage.jpg", bs.toByteArray())
.bytesUploadBuild()
.upload(new UploadCallback() {
@Override
public void onStart() {
}
@Override
public void onProgress(long currentSize, long totalSize, float progress) {
}
@Override
public void onFinish(String response) {
Toast.makeText(context, "finish", Toast.LENGTH_SHORT).show();
}
@Override
public void onError(String error) {
}
});
表单式上传本地文件:
DUtil.initFormUpload()
.url(url)
.addParam("key", "value")
.addParam("key1", "value1")
.addFile("file", "BeautyImage.jpg", new File(Environment.getExternalStorageDirectory() + "/DUtil/", "aaa.jpg"))
.fileUploadBuild()
.upload(new SimpleUploadCallback() {
@Override
public void onStart() {
super.onStart();
}
@Override
public void onFinish(String response) {
super.onFinish(response);
}
});
直接将文件作为请求体上传:
DUtil.initUpload()
.url("")
.addFile(new File(Environment.getExternalStorageDirectory() + "/DUtil/", "aaa.jpg"))
.build()
.upload(new SimpleUploadCallback() {
});
相关方法说明:
- url(String url):配置上传地址
- addParam(String key, String value):以key-value的形式添加参数
- addParams(Map<String, String> params):以map的形式添加多个参数
- addHeader(String key, String value):以key-value的形式添加请求头
- addHeaders(Map<String, String> params):以map的形式添加多个请求头
- addFile(String name, String filename, File file):以表单的形式添加本地文件,name代表<input type="file" name="file"/>中的name属性值、filename代表文件名。
- addFiles(List<UploadFile> files):通过list一次添加多个本地文件。
- addByte(String name, String filename, byte[] bytes):以表单的形式添加字节流
- addBytes(List<UploadByte> byteList):通过list一次添加多个文件字节流
- addType(String type):根据需要可以配置要上传文件的媒体类型
- addFile(File file):直接将本地文件添加到请求体
- addByte(byte[] bytes):直接将字节流添加到请求体
github地址:https://github.com/SheHuan/DUtil