轉(zhuǎn)載自:陳開華博客
Retrofit2
是目前很流行的android網(wǎng)絡(luò)框架募书,運用注解和動態(tài)代理绪囱,極大的簡化了網(wǎng)絡(luò)請求的繁瑣步驟,非常適合處理restfull
網(wǎng)絡(luò)請求莹捡。在項目中鬼吵,經(jīng)常需要上傳文件到服務(wù)器,有時候是需要上傳多個文件篮赢。網(wǎng)上文章基本都是單文件上傳教程齿椅,固定數(shù)量多文件上傳,這篇文章主要講retrofit在不確定文件數(shù)量下的多文件上傳實現(xiàn)启泣。
個人覺得有必要深入理解http協(xié)議涣脚,這樣無論使用哪個網(wǎng)絡(luò)框架,碰到類似這樣上傳的問題寥茫,一眼就能知道問題出在哪里遣蚀。因此就有必要了解http協(xié)議的上傳機制。
了解multipart/form-data
在最初的http協(xié)議中,沒有定義上傳文件的Method
芭梯,為了實現(xiàn)這個功能险耀,http協(xié)議組改造了post請求,添加了一種post規(guī)范玖喘,設(shè)定這種規(guī)范的Content-Type
為multipart/form-data;boundary=${bound}
,其中${bound}
是定義的分隔符胰耗,用于分割各項內(nèi)容(文件,key-value對),不然服務(wù)器無法正確識別各項內(nèi)容芒涡。post body里需要用到,盡量保證隨機唯一柴灯。
post格式如下:
--${bound}
Content-Disposition: form-data; name="Filename"
HTTP.pdf
--${bound}
Content-Disposition: form-data; name="file000"; filename="HTTP協(xié)議詳解.pdf"
Content-Type: application/octet-stream
%PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload"
Submit Query
--${bound}--
${bound}
是Content-Type
里boundary
的值
Retrofit2 對multipart/form-data的封裝
Retrofit其實是個網(wǎng)絡(luò)代理框架,負責封裝請求费尽,然后把請求分發(fā)給http協(xié)議具體實現(xiàn)者-httpclient
赠群。retrofit默認的httpclient
是okhttp。
既然Retrofit不實現(xiàn)http旱幼,為啥還用它呢查描。因為他方便!柏卤!
Retrofit會根據(jù)注解封裝網(wǎng)絡(luò)請求冬三,待httpclient請求完成后,把原始response內(nèi)容通過轉(zhuǎn)化器(converter
)轉(zhuǎn)化成我們需要的對象(object
)缘缚。
具體怎么使用 retrofit2,請參考:Retrofit2官網(wǎng)
那么Retrofit和okhttp怎么封裝這些multipart/form-data
上傳數(shù)據(jù)呢
- 在retrofit中:
-
@retrofit2.http.Multipart
: 標記一個請求是multipart/form-data
類型,需要和@retrofit2.http.POST
一同使用勾笆,并且方法參數(shù)必須是@retrofit2.http.Part
注解。 -
@retrofit2.http.Part
: 代表Multipart
里的一項數(shù)據(jù),即用${bound}
分隔的內(nèi)容塊桥滨。
-
- 在okhttp3中:
-
okhttp3.MultipartBody
:multipart/form-data
的抽象封裝,繼承okhttp3.RequestBody
-
okhttp3.MultipartBody.Part
:multipart/form-data
里的一項數(shù)據(jù)窝爪。
-
Service接口定義
假設(shè)服務(wù)器上傳接口返回數(shù)據(jù)類型為application/json
,字段如下
{
data: "上傳了3個文件",
msg: "訪問成功",
code: 200
}
因此需要對返回數(shù)據(jù)封裝成一個對象,考慮到復用性,封裝成泛型最佳:
public class BaseResponse<T> {
public int code;
public String msg;
public T data;
}
接著定義一個上傳的網(wǎng)絡(luò)請求Service:
public interface FileuploadService {
/**
* 通過 List<MultipartBody.Part> 傳入多個part實現(xiàn)多文件上傳
* @param parts 每個part代表一個
* @return 狀態(tài)信息
*/
@Multipart
@POST("users/image")
Call<BaseResponse<String>> uploadFilesWithParts(@Part() List<MultipartBody.Part> parts);
/**
* 通過 MultipartBody和@body作為參數(shù)來上傳
* @param multipartBody MultipartBody包含多個Part
* @return 狀態(tài)信息
*/
@POST("users/image")
Call<BaseResponse<String>> uploadFileWithRequestBody(@Body MultipartBody multipartBody);
}
由上可知齐媒,有兩種方式實現(xiàn)上傳
- 使用
@Multipart
注解方法蒲每,并用@Part
注解方法參數(shù),類型是List<okhttp3.MultipartBody.Part>
- 不使用
@Multipart
注解方法喻括,直接使用@Body
注解方法參數(shù)邀杏,類型是okhttp3.MultipartBody
可以看到,無論方法參數(shù)類型是MultipartBody.Part
還是MultipartBody
,這些類都不是Retrofit的類唬血,而是okhttp
實現(xiàn)上傳的源生類望蜡。
為什么可以這樣寫:
- Retrofit會判斷
@Body
的參數(shù)類型,如果參數(shù)類型為okhttp3.RequestBody
,則Retrofit不做包裝處理刁品,直接丟給okhttp3處理泣特。而MultipartBody
是繼承RequestBody
,因此Retrofit不會自動包裝這個對象挑随。 - 同理,Retrofit會判斷
@Part
的參數(shù)類型,如果參數(shù)類型為okhttp3.MultipartBody.Part
,則Retrofit會把RequestBody
封裝成MultipartBody
兜挨,再把Part
添加到MultipartBody
膏孟。
上傳多個文件
寫好service接口后,來看看怎么構(gòu)造MultipartBody
可以寫一個方法拌汇,用于把File
對象轉(zhuǎn)化成MultipartBody
:
public static MultipartBody filesToMultipartBody(List<File> files) {
MultipartBody.Builder builder = new MultipartBody.Builder();
for (File file : files) {
// TODO: 16-4-2 這里為了簡單起見柒桑,沒有判斷file的類型
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
builder.addFormDataPart("file", file.getName(), requestBody);
}
builder.setType(MultipartBody.FORM);
MultipartBody multipartBody = builder.build();
return multipartBody;
}
或者把File
轉(zhuǎn)化成MultipartBody.Part
:
public static List<MultipartBody.Part> filesToMultipartBodyParts(List<File> files) {
List<MultipartBody.Part> parts = new ArrayList<>(files.size());
for (File file : files) {
// TODO: 16-4-2 這里為了簡單起見,沒有判斷file的類型
RequestBody requestBody = RequestBody.create(MediaType.parse("image/png"), file);
MultipartBody.Part part = MultipartBody.Part.createFormData("file", file.getName(), requestBody);
parts.add(part);
}
return parts;
}
最后就剩下調(diào)用了
為了復用噪舀,因此把構(gòu)建Retrofit簡單封裝成一個builder類:
public class RetrofitBuilder {
private static Retrofit retrofit;
public synchronized static Retrofit buildRetrofit() {
if (retrofit == null) {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
GsonConverterFactory gsonConverterFactory = GsonConverterFactory.create(gson);
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging).retryOnConnectionFailure(true)
.build();
retrofit = new Retrofit.Builder().client(client)
.baseUrl(Config.NetURL.BASE_URL)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
}
return retrofit;
}
}
接著可以在activity里調(diào)用FileUploadService
的接口了:
private void uploadFile() {
MultipartBody body = MultipartBuilder.filesToMultipartBody(mFileList);
RetrofitBuilder.buildRetrofit().create(FileUploadService.class).uploadFileWithRequestBody(body)
.enqueue(new Callback<BaseResponse<String>>() {
@Override
public void onResponse(Call<BaseResponse<String>> call, Response<BaseResponse<String>> response) {
if (response.isSuccessful()) {
BaseResponse<String> body = response.body();
Toast.makeText(LoginActivity.this, "上傳成功:"+response.body().getMsg(), Toast.LENGTH_SHORT).show();
} else {
Log.d(TAG,"上傳失敗");
Toast.makeText(RegisterActivity.this, "上傳失敗", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(Call<BaseResponse<String>> call, Throwable t) {
Toast.makeText(RegisterActivity.this, "網(wǎng)絡(luò)連接失敗", Toast.LENGTH_SHORT).show();
}
});
}