Android Retrofit 实现(图文上传)文字(参数)和多张图片一起上传
使用Retrofit进行文件上传,肯定离不开Part & PartMap。
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
@Part MultipartBody.Part file);
}
上面是定义的接口方法,需要注意的是,这个方法不再有 @FormUrlEncoded 这个注解,而换成了 @Multipart,后面只需要在参数中增加 Part 就可以了。也许你会问,这里的 Part 和 Field 究竟有什么区别,其实从功能上讲,无非就是客户端向服务端发起请求携带参数的方式不同,并且前者可以携带的参数类型更加丰富,包括数据流。也正是因为这一点,我们可以通过这种方式来上传文件,下面我们就给出这个接口的使用方法:
//先创建 service
FileUploadService service = retrofit.create(FileUploadService.class);
//构建要上传的文件
File file = new File(filename);
RequestBody requestFile =
RequestBody.create(MediaType.parse("application/otcet-stream"), file);
MultipartBody.Part body =
MultipartBody.Part.createFormData("aFile", file.getName(), requestFile);
String descriptionString = "This is a description";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
Call call = service.upload(description, body);
call.enqueue(new Callback() {
@Override
public void onResponse(Call call,
Response response) {
System.out.println("success");
}
@Override
public void onFailure(Call call, Throwable t) {
t.printStackTrace();
}
});
在实验时,我上传了一个只包含一行文字的文件:
Visit me: http://www.println.net
那么我们去服务端看下我们的请求是什么样的:
HEADERS
Accept-Encoding: gzip
Content-Length: 470
Content-Type: multipart/form-data; boundary=9b670d44-63dc-4a8a-833d-66e45e0156ca
User-Agent: okhttp/3.2.0
X-Request-Id: 9d70e8cc-958b-4f42-b979-4c1fcd474352
Via: 1.1 vegur
Host: requestb.in
Total-Route-Time: 0
Connection: close
Connect-Time: 0
FORM/POST PARAMETERS
description: This is a description
RAW BODY
–9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name=”description”
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21This is a description
–9b670d44-63dc-4a8a-833d-66e45e0156ca
Content-Disposition: form-data; name=”aFile”; filename=”uploadedfile.txt”
Content-Type: application/otcet-stream
Content-Length: 32Visit me: http://www.println.net
–9b670d44-63dc-4a8a-833d-66e45e0156ca–
我们看到,我们上传的文件的内容出现在请求当中了。如果需要上传多个文件,就声明多个 Part 参数,或者试试 PartMap。
上面为大家展示了如何用 Retrofit 上传文件,这个上传的过程还是有那么点儿不够简练,我们只是要提供一个文件用于上传,可我们前后构造了三个对象:
天哪,肯定是哪里出了问题。实际上,Retrofit 允许我们自己定义入参和返回的类型,不过,如果这些类型比较特别,我们还需要准备相应的 Converter,也正是因为 Converter 的存在, Retrofit 在入参和返回类型上表现得非常灵活。
下面我们把刚才的 Service 代码稍作修改:
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
//注意这里的参数 "aFile" 之前是在创建 MultipartBody.Part 的时候传入的
@Part("aFile") File file);
}
现在我们把入参类型改成了我们熟悉的 File,如果你就这么拿去发请求,服务端收到的结果会让你哭了的。。。
RAW BODY
–7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name=”description”
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21This is a description
–7d24e78e-4354-4ed4-9db4-57d799b6efb7
Content-Disposition: form-data; name=”aFile”
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 35// 注意这里!!之前是文件的内容,现在变成了文件的路径
{“path”:”samples/uploadedfile.txt”}
–7d24e78e-4354-4ed4-9db4-57d799b6efb7–
服务端收到了一个文件的路径,它肯定会觉得你在逗他。
好了,不闹了,这明显是 Retrofit 在发现自己收到的实际入参是个 File 时,不知道该怎么办,情急之下给 toString了,而且还是个 JsonString(后来查证原来是使用了 GsonRequestBodyConverter。。)。
接下来我们就自己实现一个 FileRequestBodyConverter:
static class FileRequestBodyConverterFactory extends Converter.Factory {
@Override
public Converter requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return new FileRequestBodyConverter();
}
}
static class FileRequestBodyConverter implements Converter<File, RequestBody> {
@Override
public RequestBody convert(File file) throws IOException {
return RequestBody.create(MediaType.parse("application/otcet-stream"), file);
}
}
在创建 Retrofit 的时候记得配置上它:
.addConverterFactory(new FileRequestBodyConverterFactory())
这样,我们的文件内容就能上传了。来,看下结果吧:
RAW BODY
–25258f46-48b0-4a6b-a617-15318c168ed4
Content-Disposition: form-data; name=”description”
Content-Transfer-Encoding: binary
Content-Type: multipart/form-data; charset=utf-8
Content-Length: 21This is a description
–25258f46-48b0-4a6b-a617-15318c168ed4
//注意看这里,filename 没了
Content-Disposition: form-data; name=”aFile”
//多了这一句
Content-Transfer-Encoding: binary
Content-Type: application/otcet-stream
Content-Length: 32Visit me: http://www.println.net
–25258f46-48b0-4a6b-a617-15318c168ed4–
文件内容成功上传了,当然其中还存在一些问题,这个目前直接使用 Retrofit 的 Converter 还做不到,原因主要在于我们没有办法通过 Converter 直接将 File 转换为 MultiPartBody.Part,如果想要做到这一点,我们可以对 Retrofit 的源码稍作修改,这个我们下面再谈。
继续简化文件上传的接口
上面我们曾试图简化文件上传接口的使用,尽管我们已经给出了相应的 File -> RequestBody 的 Converter,不过基于 Retrofit本身的限制,我们还是不能像直接构造 MultiPartBody.Part 那样来获得更多的灵活性。这时候该怎么办?当然是 Hack~~
首先明确我们的需求:
为此,我增加了一套完整的参数解析方案:
public interface Converter<F, T> {
...
abstract class Factory {
...
//返回一个满足条件的不限制类型的 Converter
public Converter, ?> arbitraryConverter(Type originalType,
Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit){
return null;
}
}
}
需要注意的是,Retrofit 类当中也需要增加相应的方法:
public Converter arbitraryConverter(Type orignalType,
Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
return nextArbitraryConverter(null, orignalType, convertedType, parameterAnnotations, methodAnnotations);
}
public Converter nextArbitraryConverter(Converter.Factory skipPast,
Type type, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations) {
checkNotNull(type, "type == null");
checkNotNull(parameterAnnotations, "parameterAnnotations == null");
checkNotNull(methodAnnotations, "methodAnnotations == null");
int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter.Factory factory = converterFactories.get(i);
Converter, ?> converter =
factory.arbitraryConverter(type, convertedType, parameterAnnotations, methodAnnotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter) converter;
}
}
return null;
}
public class TypedFileMultiPartBodyConverterFactory extends Converter.Factory {
@Override
public Converter arbitraryConverter(Type originalType, Type convertedType, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
if (originalType == TypedFile.class && convertedType == MultipartBody.Part.class) {
return new FileRequestBodyConverter();
}
return null;
}
}
public class TypedFileMultiPartBodyConverter implements Converter<TypedFile, MultipartBody.Part> {
@Override
public MultipartBody.Part convert(TypedFile typedFile) throws IOException {
RequestBody requestFile =
RequestBody.create(typedFile.getMediaType(), typedFile.getFile());
return MultipartBody.Part.createFormData(typedFile.getName(), typedFile.getFile().getName(), requestFile);
}
}
public class TypedFile {
private MediaType mediaType;
private String name;
private File file;
public TypedFile(String name, String filepath){
this(name, new File(filepath));
}
public TypedFile(String name, File file) {
this(MediaType.parse(MediaTypes.getMediaType(file)), name, file);
}
public TypedFile(MediaType mediaType, String name, String filepath) {
this(mediaType, name, new File(filepath));
}
public TypedFile(MediaType mediaType, String name, File file) {
this.mediaType = mediaType;
this.name = name;
this.file = file;
}
public String getName() {
return name;
}
public MediaType getMediaType() {
return mediaType;
}
public File getFile() {
return file;
}
}
...
} else if (MultipartBody.Part.class.isAssignableFrom(rawParameterType)) {
return ParameterHandler.RawPart.INSTANCE;
} else {
Converter, ?> converter =
retrofit.arbitraryConverter(type, MultipartBody.Part.class, annotations, methodAnnotations);
if(converter == null) {
throw parameterError(p,
"@Part annotation must supply a name or use MultipartBody.Part parameter type.");
}
return new ParameterHandler.TypedFileHandler((Converter) converter);
}
...
static final class TypedFileHandler extends ParameterHandler<TypedFile>{
private final Converter converter;
TypedFileHandler(Converter converter) {
this.converter = converter;
}
@Override
void apply(RequestBuilder builder, TypedFile value) throws IOException {
if(value != null){
builder.addPart(converter.convert(value));
}
}
}
public interface FileUploadService {
@Multipart
@POST("upload")
Call upload(@Part("description") RequestBody description,
@Part TypedFile typedFile);
}
使用方法:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://www.println.net/")
.addConverterFactory(new TypedFileMultiPartBodyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build();
FileUploadService service = retrofit.create(FileUploadService.class);
TypedFile typedFile = new TypedFile("aFile", filename);
String descriptionString = "This is a description";
RequestBody description =
RequestBody.create(
MediaType.parse("multipart/form-data"), descriptionString);
Call call = service.upload(description, typedFile);
call.enqueue(...);
至此,我们已经通过自己的双手,让 Retrofit 点亮了自定义上传文件的技能,风骚等级更上一层楼!
摘自:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1117
http://www.chenkaihua.com/2016/04/02/retrofit2-upload-multipart-files.html?utm_source=tuicool&utm_medium=referral
http://www.jianshu.com/users/fc232a6c7db3/latest_articles
http://blog.csdn.net/u010945031/article/details/49475897