【Android】Retrofit 的一些筆記

文章目錄
  1. 1. 前言
  2. 2. Header的統一處理
  3. 3. 訪問絕對路徑
  4. 4. Map的使用避免聲明冗餘的類
  5. 5. RequestBody爲String 及 文件上傳
  6. 6. 後臺Json空數據規範
  7. 7. 空數據Void聲明
  8. 8. ResponseBody爲String
  9. 9. ResponseBody的多次讀取
  10. 10. 統一的錯誤處理

Retrofit :A type-safe HTTP client for Android and Java.

retrofit : noun. a component or accessory added to something after it has been manufactured

mocky.io: Mock your HTTP responses to test your REST API


前言

本文默認ConverterFactoryGsonConverterFactory

請求階段

  • Header的統一處理
  • 訪問絕對路徑
  • Map的使用避免聲明冗餘的類
  • RequestBody爲String 及 文件上傳

返回階段

  • 後臺Json空數據規範
  • 空數據Void聲明
  • ResponseBody爲String
  • ResponseBody的多次讀取
  • 統一的錯誤處理

Header的統一處理

使用Interceptor可爲每一個request添加統一的Header信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class OkHttpInterceptor implements okhttp3.Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request newRequest = originalRequest.newBuilder()
.header("sdkInt", Integer.toString(Build.VERSION.SDK_INT))
.header("device", Build.DEVICE)
.header("user-agent", "android.sodino")
.header("ticket", Constant.TICKET)
.header("os", "android")
.header("version", "2.6")
.header("Content-Type", "application/json")
.build();
Response response = chain.proceed(newRequest);
return response;
}
}
public class RetrofitUtil {
// 定義統一的HttpClient並添加攔截器
private static OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new OkHttpInterceptor()).build();
public static Retrofit getComonRetrofit(String baseUrl) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.callbackExecutor(ThreadPool.getThreadsExecutor())
.client(okHttpClient)
.build();
return retrofit;
}
}

訪問絕對路徑

  1. @Url
    @URL resolved against the base URL.

這種方式是動態的靈活的,不需要提前預知的。

1
2
@GET
Call<ResponseBody> list(@Url String url);
  1. endpoint爲絕對路徑

這種方式需要在編碼時提前預知,與baseUrl的理念是相沖突的,不推薦使用這種方式。

endpoint


Map的使用避免聲明冗餘的類

QueryMapQuery支持複雜的、不定數的字段。
對應的Body也可以通過定義參數類型爲Map來避免聲明冗餘的類。

以下代碼爲了Post/PutBody特別定義了個IsReead類,實現方式有些重!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class IsRead {
public boolean is_read;
}
public interface Api{
@Post // or @Put
Call<ResponseBody> reqIsRead(@Body IsRead isRead);
}
// send a post or put request
Api api = createApi();
IsRead isRead = new IsRead();
isRead.is_read = true;
api.reqIsRead(isRead);

與如下代碼的功能是相同,但更簡單明瞭的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Api{
@Post // or @Put
Call<ResponseBody> reqIsRead(@Body Map<String, Boolean> map);
}
// send a post or put request
Api api = createApi();
Map<String, Boolean> map = new HashMap<>();
map.put("is_read", true);
api.reqIsRead(map);

同樣的,在請求的返回階段,如果返回內容都是單純的key: value,那ResponseBody也可以定義爲Map
不必每個接口都有對應的數據類。


RequestBody爲String 及 文件上傳

App中嵌套的H5頁面傳給App的內容是Json格式化的字符串,並要作爲Body發起Post/Put請求,這時則希望RequestBodyString,則處理爲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Api {
@Post // or @Put
Call<ResponseBody> put(@Body RequestBody body);
}
// send a post or put request
Api api = create(Api.class);
String reqString = string;
RequestBody body = RequestBody.create(MediaType.parse("application/json"), reqString);
Call<ResponseBody> call = stringApi.post(body);

如果將MediaType改爲圖片、視頻等對應的MediaType值,則很可很方便的實現文件上傳接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface Api {
@Multipart
@POST(ACT_POST_UPLOAD_PHOTO)
Call<UploadPhoto> reqUploadPhoto(@Part MultipartBody.Part file);
}
// upload photo file
File file = getPhotoFile();
// create RequestBody instance from file
MediaType mediaType = MediaType.parse("image/jpeg");
RequestBody requestFile = RequestBody.create(mediaType, file);
// MultipartBody.Part is used to send also the actual file name
MultipartBody.Part body = MultipartBody.Part.createFormData("file", file.getName(), requestFile);
// Optional:
// add another part within the multipart request
// String descriptionString = "hello, this is description speaking";
// RequestBody description = RequestBody.create(okhttp3.MultipartBody.FORM, descriptionString);
Call<UploadPhoto> call = accountApi.reqPostUploadPhoto(/*description, */body);

後臺Json空數據規範

客戶端請求數據時,後臺對空數據的返回應該是要有規範的,
應該按Json格式返回 [] 空數組或 {} 空對象,不應什麼都不返回.

什麼都不返回會導致Json解析異常,會誤導客戶端判定連接爲CMCC/ChinaNet等假網絡,導致提示網絡異常,與實際情況不符。

喂喂,後臺同學,都是開發狗,能不能別互相傷害


空數據Void聲明

如果只需要發個請求然後根據response codeHTTP_OK(200)即可,而不需要後臺回吐額外的數據,在定義接口時可以聲明ResponseBodyVoid

1
2
3
4
public interface Api {
@GET
Call<Void> reqIamOK();
}

ResponseBody爲String

當要將後臺回吐的數據通過App傳參給內嵌的H5頁面時,這時希望ResponseBodyString,應該這麼做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d("Test", "post() respCode=" + response.code());
try {
String str = response.body().string();
// do something……
} catch (IOException e) {
e.printStackTrace();
}
}
});

ResponseBody的多次讀取

當試圖去讀取response body的原始數據時,由於是從網絡上以stream的方式讀取的,所以多次讀取的話會拋如下異常:

1
2
3
4
5
6
java.lang.IllegalStateException: closed
at com.squareup.okhttp.internal.http.HttpConnection$FixedLengthSource.read(HttpConnection.java:455)
at okio.Buffer.writeAll(Buffer.java:594)
at okio.RealBufferedSource.readByteArray(RealBufferedSource.java:87)
at com.squareup.okhttp.ResponseBody.bytes(ResponseBody.java:56)
at com.squareup.okhttp.ResponseBody.string(ResponseBody.java:82)
1
2
3
4
5
6
ResponseBody responseBody = response.body();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
String responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"))
Log.d("TAG", responseBodyString);

實現了多次讀取的功能後,就可以進行下面的統一錯誤處理了。

參考:
HttpLoggingInterceptor


統一的錯誤處理

發起一次請求後可能產生的錯誤有:

  1. 網絡問題:

    • 無網絡訪問失敗
    • 鏈接超時等IO異常
    • 假網絡鏈接
  2. 後臺提示錯誤:

    • 請求參數不規範
    • 業務邏輯錯誤,如提交的內容包含敏感詞
    • Json解析失敗
    • response code不是HTTP_OK

以上錯誤會分散在CallbackonResponse()onFailure()中去。
不利於技術層的數據統計及業務層的錯誤兼容。
那麼在做統一的錯誤處理時,目標有:

  • onResponse()是純粹的success回調,剝離了response code或解析失敗等異常。
  • 能夠讀取後臺返回的數據源進行處理後,不影響數據源的繼續傳播與解析。即上文提到的多次讀取。

能夠統一進行錯誤處理的方式有Interceptor及對Callback進行override
個人選擇Callback override的方式,個人觀點希望每個類是儘可能可複用的,對於每一次request,都有對應的Callback,那麼就不想再定義新的類(Interceptor)來處理。
而且每一個request都有新的callback的實例對象,也好進行一些個性化的錯誤處理。

新的Callback代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public abstract class MyCallback<T> implements Callback<T> {
protected boolean showToast; // 本次request的錯誤是否彈toast提醒
protected String logMark;
public MyCallback() {
showToast = true;
}
public MyCallback(boolean showToast) {
this.showToast = showToast;
}
/**
* Invoked for a received HTTP response.
* <p>
* Note: An HTTP response may still indicate an application-level failure such as a 404 or 500.
* Call {@link Response#isSuccessful()} to determine if the response indicates success.
*/
@Override
public void onResponse(Call<T> call, Response<T> response){
int respCode = response.code();
Log.d("MyCallback", "onResponse() url[" + getLogMark(call) + "]" + respCode);
if (respCode == HttpURLConnection.HTTP_OK) {
onResponse(call, response, respCode);
} else {
ResponseBody responseBody = null;
if (response != null) {
responseBody = response.errorBody();
}
noHttpOK(respCode, responseBody);
onFailure(call, null, response, respCode);
}
}
protected void noHttpOK(int respCode, ResponseBody respBody) {
int errorCode = 0;
String responseBodyString = "";
if (respBody != null) {
BufferedSource source = respBody.source();
try {
source.request(Long.MAX_VALUE); // Buffer the entire body.
} catch (IOException e) {
e.printStackTrace();
}
Buffer buffer = source.buffer();
try {
responseBodyString = buffer.clone().readString(Charset.forName("UTF-8"));
Log.d("MyCallback", "noHttpOK() " + responseBodyString);
} catch (Throwable t) {
t.printStackTrace();
}
}
ErrorEn responseEn = null;
try {
responseEn = GsonUtil.fromJson(responseBodyString, ErrorEn.class);
} catch (JsonSyntaxException jsExp) {
jsExp.printStackTrace();
}
if (respCode == HttpURLConnection.HTTP_FORBIDDEN) {
// permission error, do logout
// do Logout
Logout.do();
} else if (respCode == HttpURLConnection.HTTP_BAD_GATEWAY) {
if(showToast) {
ToastUtil.showLongToast(BaseApplication.getInstance(),
BaseApplication.getInstance().getString(R.string.error_network) + "(502)");
}
} else if (responseEn != null) {
String strMsg = responseEn.message;
if (respCode == HttpURLConnection.HTTP_PAYMENT_REQUIRED
&& responseEn.errorCode == Login.ELSE_WHERE_LOGIN) {
// 異地登錄,會彈對話框,所以不需要Toast重複提示
showToast = false;
}
if (TextUtils.isEmpty(strMsg) == false) {
if (showToast) {
ToastUtil.showToast(BaseApplication.getInstance(), strMsg);
}
}
}
}
/**
* Invoked when a network exception occurred talking to the server or when an unexpected
* exception occurred creating the request or processing the response.
*/
@Override
public void onFailure(Call<T> call, Throwable t){
if (t != null) {
if(showToast) {
ToastUtil.showLongToast(BaseApplication.getInstance(),
BaseApplication.getInstance().getString(R.string.error_network));
}
Log.d("MyCallback", "onFailure() url[" + getLogMark(call) + "]" + t.getMessage());
}
onFailure(call, t, null, -1);
}
/**
* @param respCode HttpURLConnection.responseCode.
* */
public abstract void onResponse(Call<T> call, Response<T> response, int respCode);
/**
* @param response 當服務器返回的respCode不符合預期時,此處response爲{@link Callback#onResponse(Call, Response)}中的response
* @param respCode HttpURLConnection.responseCode.如果網絡連續不成功/異常等,則值是-1.
* */
public abstract void onFailure(Call<T> call, @Nullable Throwable t, @Nullable Response<T> response, int respCode);
}

About Sodino

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章