1. 程式人生 > >Android網路框架Retrofit2使用封裝:Get/Post/檔案上傳/下載

Android網路框架Retrofit2使用封裝:Get/Post/檔案上傳/下載

背景

Android開發中的網路框架經過多年的發展,目前比較主流的就是Retrofit了,Retrofit2版本出現也有幾年了,為了方便使用,特封裝了一些關於Retrofit2的程式碼,分享給大家。

框架主要包括:

  • Get請求
  • Post請求
  • 檔案上傳
  • 檔案下載

使用效果預覽:

演示

Retrofit物件

Retrofit框架內部使用的還是OkHttp框架,在例項化的時候可以自定義OkHttpClient來實現一些個性化的設定,如超時時長、HTTPS協議支援等。

1、OkHttpClient例項

private static int TIME_OUT = 30; // 30秒超時斷開連線

public static OkHttpClient client = new OkHttpClient.Builder()
            .sslSocketFactory(sslSocketFactory, trustAllCert)
            .connectTimeout(TIME_OUT, TimeUnit.SECONDS)
            .readTimeout(TIME_OUT, TimeUnit.SECONDS)
            .writeTimeout(TIME_OUT, TimeUnit.SECONDS)
            .build();

其中,自定義了sslSocketFactory和trustAllCert證書管理器:

private static X509TrustManager trustAllCert = new X509TrustManager() {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return new java.security.cert.X509Certificate[]{};
    }
};

private static SSLSocketFactory sslSocketFactory = new SSLSocketFactoryCompat(trustAllCert);

SSLSocketFactoryCompat類是從網路上找的,這裡就不多說了,詳見原始碼部分。

2、Retrofit物件

/**
 * 網路框架單例
 * <p>因為示例中用到不同的介面地址,URL_BASE隨便寫了一個,如果是實際專案中,則設定為根路徑即可</p>
 */
private static Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(Constant.URL_BASE)
        .client(client)
        .build();

定義好OkHttpClient後,Retrofit物件的構造就很簡單,需要注意的是,一定要為retrofit物件指定好baseUrl,如果是後臺地址有多個,寫其中一個就可以了。

網路上有不少涉及到動態註冊baseUrl的文章,寫demo的時候並沒有遇到此類問題,如果有遇到的話再尋找對應方案即可。

請求框架

定義回撥

封裝的框架讓使用者傳遞一個回撥的物件,在網路請求開始、結束、異常等階段根據需要將呼叫相應的回撥介面。

該回調介面還能夠自動處理一些網路異常,並給予相應的提示(按需修改)。

/**
 * 網路請求結果處理類
 * @param <T> 請求結果封裝物件
 */
public static abstract class ResultHandler<T> {
    Context context;

    public ResultHandler(Context context) {
        this.context = context;
    }

    /**
     * 判斷網路是否未連線
     *
     * @return
     */
    public boolean isNetDisconnected() {
        return NetworkUtil.isNetDisconnected(context);
    }

    /**
     * 請求成功之前
     */
    public abstract void onBeforeResult();

    /**
     * 請求成功時
     *
     * @param t 結果資料
     */
    public abstract void onResult(T t);

    /**
     * 伺服器出錯
     */
    public void onServerError() {
        // 伺服器處理出錯
        Toast.makeText(context, R.string.net_server_error, Toast.LENGTH_SHORT).show();
    }

    /**
     * 請求失敗後的處理
     */
    public abstract void onAfterFailure();

    /**
     * 請求失敗時的處理
     *
     * @param t
     */
    public void onFailure(Throwable t) {
        if (t instanceof SocketTimeoutException || t instanceof ConnectException) {
            // 連線異常
            if (NetworkUtil.isNetworkConnected(context)) {
                // 伺服器連接出錯
                Toast.makeText(context, R.string.net_server_connected_error, Toast.LENGTH_SHORT).show();
            } else {
                // 手機網路不通
                Toast.makeText(context, R.string.net_not_connected, Toast.LENGTH_SHORT).show();
            }
        } else if (t instanceof Exception) {
            // 功能異常
            Toast.makeText(context, R.string.net_unknown_error, Toast.LENGTH_SHORT).show();
        }
    }
}

其中的string定義:

<string name="net_not_connected">傳送請求失敗,請檢查網路連線</string>
<string name="net_server_connected_error">連線伺服器失敗,請稍後重試</string>
<string name="net_server_error">伺服器處理請求失敗,請稍後重試</string>
<string name="net_server_param_error">應用資料出錯,請重新整理重試</string>
<string name="net_unknown_error">未知錯誤,請稍後重試</string>

Get請求

1、定義GetRequest

Retrofit通過定義service來發起網路請求服務,Get請求服務可以定義為:

package com.dommy.retrofitframe.network;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;

/**
 * Get請求封裝
 */
public interface GetRequest {

    /**
     * 傳送Get請求請求
     * @param url URL路徑
     * @return
     */
    @GET
    Call<ResponseBody> getUrl(@Url String url);
}

說明:

  • 需要加@GET註解;
  • url對應的引數需要加@Url註解。

2、封裝get請求方法

定義static方法:

/**
 * 傳送GET網路請求
 * @param url 請求地址
 * @param clazz 返回的資料型別
 * @param resultHandler 回撥
 * @param <T> 泛型
 */
public static <T extends BaseResult> void sendGetRequest(String url, final Class<T> clazz, final ResultHandler<T> resultHandler) {
    // 判斷網路連線狀況
    if (resultHandler.isNetDisconnected()) {
        resultHandler.onAfterFailure();
        return;
    }

    GetRequest getRequest = retrofit.create(GetRequest.class);

    // 構建請求
    Call<ResponseBody> call = getRequest.getUrl(url);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            resultHandler.onBeforeResult();
            try {
                ResponseBody body = response.body();
                if (body == null) {
                    resultHandler.onServerError();
                    return;
                }
                String string = body.string();
                T t = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create().fromJson(string, clazz);

                resultHandler.onResult(t);
            } catch (IOException e) {
                e.printStackTrace();
                resultHandler.onFailure(e);
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            resultHandler.onFailure(t);
            resultHandler.onAfterFailure();
        }
    });
}

其中,返回值的Gson轉換過程,可以根據需要選擇去掉;如果返回值都是json格式,可以保留,這樣在使用處就可以直接拿到Bean物件,比較方便。

3、發起Get請求

RetrofitRequest.sendGetRequest(url, WeatherResult.class, new RetrofitRequest.ResultHandler<WeatherResult>(this) {
    @Override
    public void onBeforeResult() {
        // 這裡可以放關閉loading
    }

    @Override
    public void onResult(WeatherResult weatherResult) {
        String weather = new Gson().toJson(weatherResult);
        tvContent.setText(weather);
    }

    @Override
    public void onAfterFailure() {
        // 這裡可以放關閉loading
    }
});

其中,WeatherResult類:

package com.dommy.retrofitframe.network.result;

import com.google.gson.JsonObject;

public class WeatherResult extends BaseResult {
    private int code;
    private String msg;
    private JsonObject data; // 資料部分也是一個bean,用JsonObject代替了

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public JsonObject getData() {
        return data;
    }

    public void setData(JsonObject data) {
        this.data = data;
    }
}

Post請求

1、定義PostRequest

Post請求服務可以定義為:

package com.dommy.retrofitframe.network;

import java.util.Map;

import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
import retrofit2.http.Url;

/**
 * Post請求封裝
 */
public interface PostRequest{

    /**
     * 傳送Post請求
     * @param url URL路徑
     * @param requestMap 請求引數
     * @return
     */
    @FormUrlEncoded
    @POST
    Call<ResponseBody> postMap(@Url String url, @FieldMap Map<String, String> requestMap);
}

說明:

  • 需要加@FormUrlEncoded、@POST註解;
  • url對應的引數需要加@Url註解。
  • post提交的引數需要加@FieldMap註解。

2、封裝post請求方法

定義static方法:

/**
 * 傳送post網路請求
 * @param url 請求地址
 * @param paramMap 引數列表
 * @param clazz 返回的資料型別
 * @param resultHandler 回撥
 * @param <T> 泛型
 */
public static <T extends BaseResult> void sendPostRequest(String url, Map<String, String> paramMap, final Class<T> clazz, final ResultHandler<T> resultHandler) {
    // 判斷網路連線狀況
    if (resultHandler.isNetDisconnected()) {
        resultHandler.onAfterFailure();
        return;
    }
    PostRequest postRequest = retrofit.create(PostRequest.class);

    // 構建請求
    Call<ResponseBody> call = postRequest.postMap(url, paramMap);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            resultHandler.onBeforeResult();
            try {
                ResponseBody body = response.body();
                if (body == null) {
                    resultHandler.onServerError();
                    return;
                }
                String string = body.string();
                T t = new Gson().fromJson(string, clazz);

                resultHandler.onResult(t);
            } catch (IOException e) {
                e.printStackTrace();
                resultHandler.onFailure(e);
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            resultHandler.onFailure(t);
            resultHandler.onAfterFailure();
        }
    });
}

3、發起Post請求

RetrofitRequest.sendPostRequest(url, paramMap, WeatherResult.class, new RetrofitRequest.ResultHandler<WeatherResult>(this) {
    @Override
    public void onBeforeResult() {
        // 這裡可以放關閉loading
    }

    @Override
    public void onResult(WeatherResult weatherResult) {
        String weather = new Gson().toJson(weatherResult);
        tvContent.setText(weather);
    }

    @Override
    public void onAfterFailure() {
        // 這裡可以放關閉loading
    }
});

檔案上傳

1、定義FileRequest

package com.dommy.retrofitframe.network;

import java.util.Map;

import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PartMap;
import retrofit2.http.Streaming;
import retrofit2.http.Url;

/**
 * 檔案上傳請求封裝
 */
public interface FileRequest {

    /**
     * 上傳檔案請求
     * @param url      URL路徑
     * @param paramMap 請求引數
     * @return
     */
    @Multipart
    @POST
    Call<ResponseBody> postFile(@Url String url, @PartMap Map<String, RequestBody> paramMap);

    /**
     * 下載檔案get請求
     * @param url 連結地址
     * @return
     */
    @Streaming
    @GET
    Call<ResponseBody> download(@Url String url);
}

說明:

  • 檔案上傳需要額外新增@Multipart、@PartMap註解;
  • 檔案下載需要額外新增@Streaming註解。

2、封裝檔案上傳方法

定義static方法:

/**
 * 傳送上傳檔案網路請求
 * @param url 請求地址
 * @param file 檔案
 * @param clazz 返回的資料型別
 * @param resultHandler 回撥
 * @param <T> 泛型
 */
public static <T extends BaseResult> void fileUpload(String url, File file, final Class<T> clazz, final ResultHandler<T> resultHandler) {
    // 判斷網路連線狀況
    if (resultHandler.isNetDisconnected()) {
        resultHandler.onAfterFailure();
        return;
    }
    FileRequest fileRequest = retrofit.create(FileRequest.class);

    Map<String, RequestBody> paramMap = new HashMap<>();
    addMultiPart(paramMap, "file", file);

    // 構建請求
    Call<ResponseBody> call = fileRequest.postFile(url, paramMap);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            resultHandler.onBeforeResult();
            try {
                ResponseBody body = response.body();
                if (body == null) {
                    resultHandler.onServerError();
                    return;
                }
                String string = body.string();
                T t = new Gson().fromJson(string, clazz);

                resultHandler.onResult(t);
            } catch (IOException e) {
                e.printStackTrace();
                resultHandler.onFailure(e);
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            resultHandler.onFailure(t);
            resultHandler.onAfterFailure();
        }
    });
}

3、發起檔案上傳請求

因為沒有找到現成的上傳檔案介面,所以這裡就隨便弄了個介面做了個例子。自己寫的時候在後臺做一個介面檔案的介面就行了。

File file = null;
try {
    // 通過新建檔案替代檔案定址
    file = File.createTempFile("abc", "txt");
} catch (IOException e) {
}
String url = Constant.URL_LOGIN;
RetrofitRequest.fileUpload(url, file, BaseResult.class, new RetrofitRequest.ResultHandler<BaseResult>(this) {
    @Override
    public void onBeforeResult() {
        // 這裡可以放關閉loading
    }

    @Override
    public void onResult(BaseResult baseResult) {
        tvContent.setText("上傳成功");
    }

    @Override
    public void onAfterFailure() {
        // 這裡可以放關閉loading
    }
});

檔案下載

基於FileRequest的定義,封裝fileDownload方法即可。

1、定義DownloadHandler

因為檔案下載過程中涉及到進度的計算、顯示,所以這裡需要定義一個DownloadHandler回撥:

/**
 * 檔案下載回撥
 */
public interface DownloadHandler {
    /**
     * 接收到資料體
     * @param body 響應體
     */
    public void onBody(ResponseBody body);

    /**
     * 檔案下載出錯
     */
    public void onError();
}

2、定義Retrofit物件

為了防止回撥方法內部涉及到介面操作,出現NetworkOnMainThreadException問題,特別自定義一個回撥方法執行器executorService。

ExecutorService executorService = Executors.newFixedThreadPool(1);
// 網路框架
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(Constant.URL_BASE)
        .callbackExecutor(executorService)
        .build();

3、封裝檔案下載方法

/**
 * 檔案下載
 * @param url 請求地址
 * @param downloadHandler 回撥介面
 */
public static void fileDownload(String url, final DownloadHandler downloadHandler) {
    // 回撥方法執行器,定義回撥在子執行緒中執行,避免Callback返回到MainThread,導致檔案下載出現NetworkOnMainThreadException
    ExecutorService executorService = Executors.newFixedThreadPool(1);
    // 網路框架
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constant.URL_BASE)
            .callbackExecutor(executorService)
            .build();

    FileRequest fileRequest = retrofit.create(FileRequest.class);
    // 構建請求
    Call<ResponseBody> call = fileRequest.download(url);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            if (response.isSuccessful()) {
                // 寫入檔案
                downloadHandler.onBody(response.body());
            } else {
                downloadHandler.onError();
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            downloadHandler.onError();
        }
    });
}

4、檔案下載示例

RetrofitRequest.fileDownload(Constant.URL_DOWNLOAD, new RetrofitRequest.DownloadHandler() {
    @Override
    public void onBody(ResponseBody body) {
        if (!writeResponseBodyToDisk(body)) {
            mHandler.sendEmptyMessage(DOWNLOAD_ERROR);
        }
    }

    @Override
    public void onError() {
        mHandler.sendEmptyMessage(DOWNLOAD_ERROR);
    }
});

其中涉及到的物件和方法:

private static final int DOWNLOAD_ING = 1;// 下載中
private static final int DOWNLOAD_FINISH = 2;// 下載結束
private static final int DOWNLOAD_ERROR = -1;// 下載出錯

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case DOWNLOAD_ING:
                // 正在下載
                showProgress();
                break;
            case DOWNLOAD_FINISH:
                // 下載完成
                onDownloadFinish();
                break;
            case DOWNLOAD_ERROR:
                // 出錯
                onDownloadError();
                break;
        }
    }
};
    
/**
 * 寫檔案入磁碟
 *
 * @param body 請求結果
 * @return boolean 是否下載寫入成功
 */
private boolean writeResponseBodyToDisk(ResponseBody body) {
    savePath = StorageUtil.getDownloadPath();
    File apkFile = new File(savePath, fileName);
    InputStream inputStream = null;
    OutputStream outputStream = null;
    try {
        byte[] fileReader = new byte[4096];
        // 獲取檔案大小
        long fileSize = body.contentLength();
        long fileSizeDownloaded = 0;
        inputStream = body.byteStream();
        outputStream = new FileOutputStream(apkFile);

        // byte轉Kbyte
        BigDecimal bd1024 = new BigDecimal(1024);
        totalByte = new BigDecimal(fileSize).divide(bd1024, BigDecimal.ROUND_HALF_UP).setScale(0).intValue();

        // 只要沒有取消就一直下載資料
        while (!cancelUpdate) {
            int read = inputStream.read(fileReader);
            if (read == -1) {
                // 下載完成
                mHandler.sendEmptyMessage(DOWNLOAD_FINISH);
                break;
            }
            outputStream.write(fileReader, 0, read);
            fileSizeDownloaded += read;
            // 計算進度
            progress = (int) (((float) (fileSizeDownloaded * 100.0 / fileSize)));
            downByte = new BigDecimal(fileSizeDownloaded).divide(bd1024, BigDecimal.ROUND_HALF_UP).setScale(0).intValue();
            // 子執行緒中,藉助handler更新介面
            mHandler.sendEmptyMessage(DOWNLOAD_ING);
        }
        outputStream.flush();
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    } finally {
        if (outputStream != null) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 顯示下載進度
 */
private void showProgress() {
    String text = progress + "%  |  " + downByte + "Kb / " + totalByte + "Kb";
    tvContent.setText(text);
}

/**
 * 下載出錯處理
 */
private void onDownloadError() {
    tvContent.setText("下載出錯");
}

/**
 * 下載完成
 */
private void onDownloadFinish() {
    tvContent.setText("下載已完成");
}

總結

Retrofit框架在APP中具有出現的效能,鑑於API的呼叫相對複雜(容易冗餘),特封裝了一套框架來支援日常開發使用。開發者可根據自身需要定義回撥介面包含的方法,裁切部分功能。

特別感謝:

原始碼