在上一篇基于retrofit的網絡架構的終極封裝(一)中介紹了頂層api的設計.這裡再沿着代碼走向往裡說.
由于這裡講的是retrofit的封裝性使用,是以一些retrofit基礎性的使用和配置這裡就不講了.
參數怎麼傳遞到retrofit層的?
所有網絡請求相關的參數和配置全部通過第一層的api和鍊式調用封裝到了ConfigInfo中,最後在start()方法中調用retrofit層,開始網絡請求.
@Override
public ConfigInfo start(ConfigInfo configInfo) {
String url = Tool.appendUrl(configInfo.url, isAppendUrl());//組拼baseUrl和urltail
configInfo.url = url;
configInfo.listener.url = url;
//todo 這裡token還可能在請求頭中,應加上此類情況的自定義.
if (configInfo.isAppendToken){
Tool.addToken(configInfo.params);
}
if (configInfo.loadingDialog != null && !configInfo.loadingDialog.isShowing()){
try {//預防badtoken最簡便和直接的方法
configInfo.loadingDialog.show();
}catch (Exception e){
}
}
if (getCache(configInfo)){//異步,去拿緩存--隻針對String類型的請求
return configInfo;
}
T request = generateNewRequest(configInfo);//根據類型生成/執行不同的請求對象
return configInfo;
}
分類生成/執行各類請求:
private T generateNewRequest(ConfigInfo configInfo) {
int requestType = configInfo.type;
switch (requestType){
case ConfigInfo.TYPE_STRING:
case ConfigInfo.TYPE_JSON:
case ConfigInfo.TYPE_JSON_FORMATTED:
return newCommonStringRequest(configInfo);
case ConfigInfo.TYPE_DOWNLOAD:
return newDownloadRequest(configInfo);
case ConfigInfo.TYPE_UPLOAD_WITH_PROGRESS:
return newUploadRequest(configInfo);
default:return null;
}
}
是以,對retrofit的使用,隻要實作以下三個方法就行了:
如果切換到volley或者其他網絡架構,也是實作這三個方法就好了.
newCommonStringRequest(configInfo),
newDownloadRequest(configInfo);
newUploadRequest(configInfo)
String類請求在retrofit中的封裝:
@Override
protected Call newCommonStringRequest(final ConfigInfo configInfo) {
Call call;
if (configInfo.method == HttpMethod.GET){
call = service.executGet(configInfo.url,configInfo.params);
}else if (configInfo.method == HttpMethod.POST){
if(configInfo.paramsAsJson){//參數在請求體以json的形式發出
String jsonStr = MyJson.toJsonStr(configInfo.params);
Log.e("dd","jsonstr request:"+jsonStr);
RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);
call = service.executeJsonPost(configInfo.url,body);
}else {
call = service.executePost(configInfo.url,configInfo.params);
}
}else {
configInfo.listener.onError("不是get或post方法");//暫時不考慮其他方法
call = null;
return call;
}
configInfo.tagForCancle = call;
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, final Response response) {
if (!response.isSuccessful()){
configInfo.listener.onCodeError("http錯誤碼為:"+response.code(),response.message(),response.code());
Tool.dismiss(configInfo.loadingDialog);
return;
}
String string = "";
try {
string = response.body().string();
Tool.parseStringByType(string,configInfo);
Tool.dismiss(configInfo.loadingDialog);
} catch (final IOException e) {
e.printStackTrace();
configInfo.listener.onError(e.toString());
Tool.dismiss(configInfo.loadingDialog);
}
}
@Override
public void onFailure(Call call, final Throwable t) {
configInfo.listener.onError(t.toString());
Tool.dismiss(configInfo.loadingDialog);
}
});
return call;
}
service中通用方法的封裝
既然要封裝,肯定就不能用retrofit的正常用法:ApiService接口裡每個接口文檔上的接口都寫一個方法,而是應該用QueryMap/FieldMap注解,接受一個以Map形式封裝好的鍵值對.這個與我們上一層的封裝思路和形式都是一樣的.
@GET()
Call executGet(@Url String url, @QueryMap Map maps);
@FormUrlEncoded
@POST()
Call executePost(@Url String url, @FieldMap Map map);
@POST()
Call executeJsonPost(@Url String url, @Body RequestBody body);
post參數體以json的形式發出時需要注意:
retrofit其實有請求時傳入一個javabean的注解方式,确實可以在架構内部轉換成json.但是不适合封裝.
其實很簡單,搞清楚以json形式發出參數的本質: 請求體中的json本質上還是一個字元串.那麼可以将Map攜帶過來的參數轉成json字元串,然後用RequestBody包裝一層就好了:
String jsonStr = MyJson.toJsonStr(configInfo.params);
RequestBody body = RequestBody.create(MediaType.parse("application/json;charset=UTF-8"), jsonStr);
call = service.executeJsonPost(configInfo.url,body);
不采用retrofit的json轉換功能:
Call的泛型不能采用二次泛型的形式--retrofit架構不接受:
@GET()
Call> getStandradJson(@Url String url, @QueryMap Map maps);
//注:BaseNetBean就是三個标準字段的json:
public class BaseNetBean{
public int code;
public String msg;
public T data;
}
這樣寫會抛出異常:
報的錯誤
Method return type must not include a type variable or wildcard: retrofit2.Call
JakeWharton的回複:
You cannot. Type information needs to be fully known at runtime in order for deserialization to work.
因為上面的原因,我們隻能通過retrofit發請求,傳回一個String,自己去解析.但這也有坑:
1.不能寫成下面的形式:
@GET()
Call executGet(@Url String url, @QueryMap Map maps);
你以為指定泛型為String它就傳回String,不,你還太年輕了.
這裡的泛型,意思是,使用retrofit内部的json轉換器,将response裡的資料轉換成一個實體類xxx,比如UserBean之類的,而String類明顯不是一個有效的實體bean類,自然轉換失敗.
是以,要讓retrofit不适用内置的json轉換功能,你應該直接指定類型為ResponseBody:
@GET()
Call executGet(@Url String url, @QueryMap Map maps);
2.既然不采用retrofit内部的json轉換功能,那就要在回調那裡自己拿到字元串,用自己的json解析了.那麼坑又來了:
泛型擦除:
回調接口上指定泛型,在回調方法裡直接拿到泛型,這是在java裡很常見的一個泛型接口設計:
public abstract class MyNetListener{
public abstract void onSuccess(T response,String resonseStr);
....
}
//使用:
call.enqueue(new Callback() {
@Override
public void onResponse(Call call, final Response response) {
String string = response.body().string();
Gson gson = new Gson();
Type objectType = new TypeToken() {}.getType();
final T bean = gson.fromJson(string,objectType);
configInfo.listener.onSuccess(bean,string);
...
}
...
}
但是,抛出異常:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to xxx
這是因為在運作過程中,通過泛型傳入的類型T丢失了,是以無法轉換,這叫做泛型擦除:.
要解析的話,還是老老實實傳入javabean的class吧.是以在最頂層的API裡,有一個必須傳的Class clazz:
postStandardJson(String url, Map map, Class clazz, MyNetListener listener)
綜上,我們需要傳入class對象,完全自己去解析json.解析已封裝成方法.也是根據三個不同的小類型(字元串,一般json,标準json)
這裡處理緩存時,如果要緩存内容,當然是緩存成功的内容,失敗的就不必緩存了.
Tool.parseStringByType(string,configInfo);
public static void parseStringByType(final String string, final ConfigInfo configInfo) {
switch (configInfo.type){
case ConfigInfo.TYPE_STRING:
//緩存
cacheResponse(string, configInfo);
//處理結果
configInfo.listener.onSuccess(string, string);
break;
case ConfigInfo.TYPE_JSON:
parseCommonJson(string,configInfo);
break;
case ConfigInfo.TYPE_JSON_FORMATTED:
parseStandJsonStr(string, configInfo);
break;
}
}
json解析架構選擇,gson,fastjson随意,不過最好也是自己再包一層api:
public static T parseObject(String str,Class clazz){
// return new Gson().fromJson(str,clazz);
return JSON.parseObject(str,clazz);
}
注意區分傳回的是jsonObject還是jsonArray,有不同的解析方式和回調.
private static void parseCommonJson( String string, ConfigInfo configInfo) {
if (isJsonEmpty(string)){
configInfo.listener.onEmpty();
}else {
try{
if (string.startsWith("{")){
E bean = MyJson.parseObject(string,configInfo.clazz);
configInfo.listener.onSuccessObj(bean ,string,string,0,"");
cacheResponse(string, configInfo);
}else if (string.startsWith("[")){
List beans = MyJson.parseArray(string,configInfo.clazz);
configInfo.listener.onSuccessArr(beans,string,string,0,"");
cacheResponse(string, configInfo);
}else {
configInfo.listener.onError("不是标準json格式");
}
}catch (Exception e){
e.printStackTrace();
configInfo.listener.onError(e.toString());
}
}
}
标準json的解析:
三個字段對應的資料直接用jsonObject.optString來取:
JSONObject object = null;
try {
object = new JSONObject(string);
} catch (JSONException e) {
e.printStackTrace();
configInfo.listener.onError("json 格式異常");
return;
}
String key_data = TextUtils.isEmpty(configInfo.key_data) ? NetDefaultConfig.KEY_DATA : configInfo.key_data;
String key_code = TextUtils.isEmpty(configInfo.key_code) ? NetDefaultConfig.KEY_CODE : configInfo.key_code;
String key_msg = TextUtils.isEmpty(configInfo.key_msg) ? NetDefaultConfig.KEY_MSG : configInfo.key_msg;
final String dataStr = object.optString(key_data);
final int code = object.optInt(key_code);
final String msg = object.optString(key_msg);
注意,optString後字元串為空的判斷:一個字段為null時,optString的結果是字元串"null"而不是null
public static boolean isJsonEmpty(String data){
if (TextUtils.isEmpty(data) || "[]".equals(data)
|| "{}".equals(data) || "null".equals(data)) {
return true;
}
return false;
}
然後就是相關的code情況的處理和回調:
狀态碼為未登入時,執行自動登入的邏輯,自動登入成功後再重發請求.登入不成功才執行unlogin()回調.
注意data字段可能是一個普通的String,而不是json.
private static void parseStandardJsonObj(final String response, final String data, final int code,
final String msg, final ConfigInfo configInfo){
int codeSuccess = configInfo.isCustomCodeSet ? configInfo.code_success : BaseNetBean.CODE_SUCCESS;
int codeUnFound = configInfo.isCustomCodeSet ? configInfo.code_unFound : BaseNetBean.CODE_UN_FOUND;
int codeUnlogin = configInfo.isCustomCodeSet ? configInfo.code_unlogin : BaseNetBean.CODE_UNLOGIN;
if (code == codeSuccess){
if (isJsonEmpty(data)){
if(configInfo.isResponseJsonArray()){
configInfo.listener.onEmpty();
}else {
configInfo.listener.onError("資料為空");
}
}else {
try{
if (data.startsWith("{")){
final E bean = MyJson.parseObject(data,configInfo.clazz);
configInfo.listener.onSuccessObj(bean ,response,data,code,msg);
cacheResponse(response, configInfo);
}else if (data.startsWith("[")){
final List beans = MyJson.parseArray(data,configInfo.clazz);
configInfo.listener.onSuccessArr(beans,response,data,code,msg);
cacheResponse(response, configInfo);
}else {//如果data的值是一個字元串,而不是标準json,那麼直接傳回
if (String.class.equals(configInfo.clazz) ){//此時,E也應該是String類型.如果有誤,會抛出到下面catch裡
configInfo.listener.onSuccess((E) data,data);
}else {
configInfo.listener.onError("不是标準的json資料");
}
}
}catch (final Exception e){
e.printStackTrace();
configInfo.listener.onError(e.toString());
return;
}
}
}else if (code == codeUnFound){
configInfo.listener.onUnFound();
}else if (code == codeUnlogin){
//自動登入
configInfo.client.autoLogin(new MyNetListener() {
@Override
public void onSuccess(Object response, String resonseStr) {
configInfo.client.resend(configInfo);
}
@Override
public void onError(String error) {
super.onError(error);
configInfo.listener.onUnlogin();
}
});
}else {
configInfo.listener.onCodeError(msg,"",code);
}
}
檔案下載下傳
先不考慮多線程下載下傳和斷點續傳的問題,就單單檔案下載下傳而言,用retrofit寫還是挺簡單的
1.讀寫的逾時時間的設定:
不能像上面字元流類型的請求一樣設定多少s,而應該設為0,也就是不限時:
OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)
.connectTimeout(30, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //設定逾時
2.接口需要聲明為流式下載下傳:
@Streaming //流式下載下傳,不加這個注解的話,會整個檔案位元組數組全部加載進記憶體,可能導緻oom
@GET
Call download(@Url String fileUrl);
3.聲明了流式下載下傳後,就能從回調而來的ResponseBody中拿到輸入流(body.byteStream()),然後開子線程寫到本地檔案中去.
這裡用的是一個異步任務架構,其實用Rxjava更好.
SimpleTask simple = new SimpleTask() {
@Override
protected Boolean doInBackground() {
return writeResponseBodyToDisk(response.body(),configInfo.filePath);
}
@Override
protected void onPostExecute(Boolean result) {
Tool.dismiss(configInfo.loadingDialog);
if (result){
configInfo.listener.onSuccess(configInfo.filePath,configInfo.filePath);
}else {
configInfo.listener.onError("檔案下載下傳失敗");
}
}
};
simple.execute();
進度回調的兩種實作方式
最簡單的,網絡流寫入到本地檔案時,獲得進度(writeResponseBodyToDisk方法裡)
byte[] fileReader = new byte[4096];
long fileSize = body.contentLength();
long fileSizeDownloaded = 0;
inputStream = body.byteStream();
outputStream = new FileOutputStream(futureStudioIconFile);
while (true) {
int read = inputStream.read(fileReader);
if (read == -1) {
break;
}
outputStream.write(fileReader, 0, read);
fileSizeDownloaded += read;
Log.d("io", "file download: " + fileSizeDownloaded + " of " + fileSize);// 這裡也可以實作進度監聽
}
利用okhttp的攔截器
1.添加下載下傳時更新進度的攔截器
okHttpClient .addInterceptor(new ProgressInterceptor())
2.ProgressInterceptor:實作Interceptor接口的intercept方法,攔截網絡響應
@Override
public Response intercept(Interceptor.Chain chain) throws IOException{
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder().body(new ProgressResponseBody(originalResponse.body(),chain.request().url().toString())).build();
}
3 ProgressResponseBody: 繼承 ResponseBody ,在内部網絡流傳輸過程中讀取進度:
public class ProgressResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private BufferedSource bufferedSource;
private String url;
public ProgressResponseBody(ResponseBody responseBody,String url) {
this.responseBody = responseBody;
this.url = url;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
long timePre = 0;
long timeNow;
private Source source(final Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
timeNow = System.currentTimeMillis();
if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次狀态
timePre = timeNow;
EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(),
totalBytesRead == responseBody.contentLength(),url));
}
return bytesRead;
}
};
}
}
進度資料以event的形式傳出(采用Eventbus),在listener中接收
一般進度資料用于更新UI,是以最好設定資料傳出的時間間隔,不要太頻繁:
事件的發出:
timeNow = System.currentTimeMillis();
if (timeNow - timePre > NetDefaultConfig.PROGRESS_INTERMEDIATE || totalBytesRead == responseBody.contentLength()){//至少300ms才更新一次狀态
timePre = timeNow;
EventBus.getDefault().post(new ProgressEvent(totalBytesRead,responseBody.contentLength(), totalBytesRead == responseBody.contentLength(),url));
}
事件的接收(MyNetListener對象中):
注意: MyNetListener與url綁定,以防止不同下載下傳間的進度錯亂.
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessage(ProgressEvent event){
if (event.url.equals(url)){
onProgressChange(event.totalLength,event.totalBytesRead);
if (event.done){
unRegistEventBus();
onFinish();
}
}
}
檔案上傳
檔案上傳相對于普通post請求有差別,你非常需要了解http檔案上傳的協定:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI3cTN9gmJ3MjN9c3PzIzN4gjN3ADMwADMwYDNx8CXlR3btVmcvw1Ztl2Lc12bj5CdsVXYmRnbl12ZlN3Lc9CX6MHc0RHaiojIsJye.jpg)
1.送出一個表單,如果包含檔案上傳,那麼必須指定類型為multipart/form-data.這個在retrofit中通過@Multipart注解指定即可.
2.表單中還有其他鍵值對也要一同傳遞,在retrofit中通過@QueryMap以map形式傳入,這個與普通post請求一樣
3.伺服器接收檔案的字段名,以及上傳的檔案路徑,通過@PartMap以map形式傳入.這裡的字段名對應請求體中Content-Disposition中的name字段的值.大多數伺服器預設是file.(因為SSH架構預設的是file?)
4.請求體的content-type用于辨別檔案的具體MIME類型.在retrofit中,是在建構請求體RequestBody時指定的.需要我們指定.
那麼如何獲得一個檔案的MIMe類型呢?讀檔案的字尾名的話,不靠譜.最佳方式是讀檔案頭,從檔案頭中拿到MIME類型.不用擔心,Android有相關的api的
綜上,相關的封裝如下:
同下載下傳一樣,配置httpclient時,讀和寫的逾時時間都要置為0
OkHttpClient client=httpBuilder.readTimeout(0, TimeUnit.SECONDS)
.connectTimeout(0, TimeUnit.SECONDS).writeTimeout(0, TimeUnit.SECONDS) //設定逾時
ApiService中通用接口的定義
@POST()
@Multipart
Call uploadWithProgress(@Url String url,@QueryMap Map options,@PartMap Map fileParameters) ;
key-filepath到key-RequestBody的轉換:
這裡的回調就不用開背景線程了,因為流是在請求體中,而retrofit已經幫我們搞定了請求過程的背景執行.
protected Call newUploadRequest(final ConfigInfo configInfo) {
if (serviceUpload == null){
initUpload();
}
configInfo.listener.registEventBus();
Map requestBodyMap = new HashMap<>();
if (configInfo.files != null && configInfo.files.size() >0){
Map files = configInfo.files;
int count = files.size();
if (count>0){
Set> set = files.entrySet();
for (Map.Entry entry : set){
String key = entry.getKey();
String value = entry.getValue();
File file = new File(value);
String type = Tool.getMimeType(file);//拿到檔案的實際類型
Log.e("type","mimetype:"+type);
UploadFileRequestBody fileRequestBody = new UploadFileRequestBody(file, type,configInfo.url);
requestBodyMap.put(key+"\"; filename=\"" + file.getName(), fileRequestBody);
}
}
}
Call call = service.uploadWithProgress(configInfo.url,configInfo.params,requestBodyMap);
注意,RequestBody中的content-type不是multipart/form-data,而是檔案的實際類型.multipart/form-data是請求頭中的檔案上傳的統一type.
public class UploadFileRequestBody extends RequestBody {
private RequestBody mRequestBody;
private BufferedSink bufferedSink;
private String url;
public UploadFileRequestBody(File file,String mimeType,String url) {
// this.mRequestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
this.mRequestBody = RequestBody.create(MediaType.parse(mimeType), file);
this.url = url;
}
@Override
public MediaType contentType() {
return mRequestBody.contentType();
}
進度的回調
封裝在UploadFileRequestBody中,無需通過okhttp的攔截器實作,因為可以在建構RequestBody的時候就包裝好(看上面代碼),就沒必要用攔截器了.
最後的話
到這裡,主要的請求執行和回調就算講完了,但還有一些,比如緩存控制,登入狀态的維護,以及cookie管理,請求的取消,gzip壓縮,本地時間校準等等必需的輔助功能的實作和維護,這些将在下一篇文章進行解析.
代碼