1. 程式人生 > >Retrofit2 multpart多檔案上傳詳解

Retrofit2 multpart多檔案上傳詳解

Retrofit2是目前很流行的android網路框架,運用註解和動態代理,極大的簡化了網路請求的繁瑣步驟,非常適合處理restfull網路請求。在專案中,經常需要上傳檔案到伺服器,有時候是需要上傳多個檔案。網上文章基本都是單檔案上傳教程,這篇文章主要講retrofit的多檔案上傳實現。
個人覺得有必要深入理解http協議,這樣無論使用哪個網路框架,碰到類似這樣上傳的問題,一眼就能知道問題出在哪裡。因此就有必要了解http協議的上傳機制。

瞭解 multipart/form-data

在最初的http協議中,沒有定義上傳檔案的Method,為了實現這個功能,http協議組改造了post請求,添加了一種post規範,設定這種規範的Content-Type為multipart/form-data;boundary=b

ound,{bound}是定義的分隔符,用於分割各項內容(檔案,key-value對),不然伺服器無法正確識別各項內容。post body裡需要用到,儘量保證隨機唯一。

post格式如下:

–${bound}
Content-Disposition: form-data; name=”Filename”

HTTP.pdf
–${bound}
Content-Disposition: form-data; name=”file000”; filename=”HTTP協議詳解.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其實是個網路代理框架,負責封裝請求,然後把請求分發給http協議具體實現者-httpclient。retrofit預設的httpclient是okhttp。

既然Retrofit不實現http,為啥還用它呢。因為他方便!!
Retrofit會根據註解封裝網路請求,待httpclient請求完成後,把原始response內容通過轉化器(converter)轉化成我們需要的物件(object)。

具體怎麼使用 retrofit2,請參考: Retrofit2官網

那麼Retrofit和okhttp怎麼封裝這些multipart/form-data上傳資料呢

  • 在retrofit中:
    @retrofit2.http.Multipart: 標記一個請求是multipart/form-data型別,需要和@retrofit2.http.POST一同使用,並且方法引數必須是@retrofit2.http.Part註解。
    @retrofit2.http.Part: 代表Multipart裡的一項資料,即用${bound}分隔的內容塊。
  • 在okhttp3中:
    okhttp3.MultipartBody: multipart/form-data的抽象封裝,繼承okhttp3.RequestBody
    okhttp3.MultipartBody.Part: multipart/form-data裡的一項資料。

Service介面定義

假設伺服器上傳介面返回資料型別為application/json,欄位如下

{
data: "上傳了3個檔案",
msg: "訪問成功",
code: 200
}

因此需要對返回資料封裝成一個物件,考慮到複用性,封裝成泛型最佳:

public class BaseResponse<T> {
    public int code;
    public String msg;
    public T data;
}

接著定義一個上傳的網路請求Service:

public interface FileuploadService {
    /**
     * 通過 List<MultipartBody.Part> 傳入多個part實現多檔案上傳
     * @param parts 每個part代表一個
     * @return 狀態資訊
     */
    @Multipart
    @POST("users/image")
    Call<BaseResponse<String>> uploadFilesWithParts(@Part() List<MultipartBody.Part> parts);


    /**
     * 通過 MultipartBody和@body作為引數來上傳
     * @param multipartBody MultipartBody包含多個Part
     * @return 狀態資訊
     */
    @POST("users/image")
    Call<BaseResponse<String>> uploadFileWithRequestBody(@Body MultipartBody multipartBody);
}

由上可知,有兩種方式實現上傳

  • 使用@Multipart註解方法,並用@Part註解方法引數,型別是List< okhttp3.MultipartBody.Part>
  • 不使用@Multipart註解方法,直接使用@Body註解方法引數,型別是okhttp3.MultipartBody

可以看到,無論方法引數型別是MultipartBody.Part還是MultipartBody,這些類都不是Retrofit的類,而是okhttp實現上傳的源生類。

為什麼可以這樣寫:

  • Retrofit會判斷@Body的引數型別,如果引數型別為okhttp3.RequestBody,則Retrofit不做包裝處理,直接丟給okhttp3處理。而MultipartBody是繼承RequestBody,因此Retrofit不會自動包裝這個物件。
  • 同理,Retrofit會判斷@Part的引數型別,如果引數型別為okhttp3.MultipartBody.Part,則Retrofit會把RequestBody封裝成MultipartBody,再把Part新增到MultipartBody。

上傳多個檔案

寫好service介面後,來看看怎麼構造MultipartBody
可以寫一個方法,用於把File物件轉化成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轉化成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;
    }

最後就剩下呼叫了

為了複用,因此把構建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裡呼叫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, "網路連線失敗", Toast.LENGTH_SHORT).show();
            }
        });
    }