0%

[Android] Retrofit應用

紀錄開發上常遇到的問題,避免重複踩坑。

紀錄API請求和回應

可使用寫好的套件 HttpLoggingInterceptor,參考https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor

或自行實作。

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
public class LogInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
String bodyString = bodyToString(request);
Log.d("Mike", String.format("[%X] Request to %s\n%s",
chain.hashCode(), request.url(), bodyString));

Response response;
try {
response = chain.proceed(request);
} catch (SocketTimeoutException e) {
Log.d("Mike", String.format("[%X] Response timeout for %s",
chain.hashCode(), request.url()));
throw e;
}
ResponseBody responseBody = response.body();
String responseBodyString = responseBody.string();
Log.d("Mike", String.format("[%X] Response to %s(%d)\n%s",
chain.hashCode(), response.request().url(), response.code(), responseBodyString));

return response.newBuilder().body(
ResponseBody.create(responseBody.contentType(), responseBodyString.getBytes())).build();
}

private String bodyToString(Request request) {
try {
Request copy = request.newBuilder().build();
Buffer buffer = new Buffer();
if (copy.body() != null) {
copy.body().writeTo(buffer);
return buffer.readUtf8();
}
return "";
} catch (IOException e) {
return "";
}
}
}

若要印出headers,可加上這段。

注意:不要使用response.headers().names()跑for each處理,例如回傳的Set-Cookie有兩個,但該寫法只會印出一個。

1
2
3
4
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
Log.d("Mike", headers.name(i) + ": " + headers.value(i));
}

請求加上token認證

例如使用Google API需要獲得token後,才有權限使用。

1
2
3
4
5
6
7
8
9
public class TokenInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.header("Authorization", "Bearer " + token)
.build();
return chain.proceed(request);
}
}

保持連線和Cookie

參考https://stackoverflow.com/questions/36706795/how-to-keep-session-using-retrofit-okhttpclient

1
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.3'
1
2
3
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
.cookieJar(new JavaNetCookieJar(new CookieManager()))
.build();

修改FieldMap

使用情境是有多個API都要傳同個欄位和值,例如UUID或時間戳記。

使用下列方式可對所有API都加上該值;但若是遇到只有部分需要,則不適用。建議改用Builder類別包住Map操作,然後將常用的欄位寫成public的method。

參考https://stackoverflow.com/questions/56515769/okhttp3-interceptor-add-fields-to-request-body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FieldMapInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if ("POST".equals(request.method()) && request.body() instanceof FormBody) {
FormBody.Builder bodyBuilder = new FormBody.Builder();
FormBody formBody = (FormBody) request.body();
for (int i = 0; i < formBody.size(); i++) {
bodyBuilder.addEncoded(formBody.encodedName(i), formBody.encodedValue(i));
}
bodyBuilder.add("key", "value");
request = request.newBuilder().post(bodyBuilder.build()).build();
}
return chain.proceed(request);
}
}

上送檔案(body為raw)

以Google Photos API的uploads為例。用Jetpack新提供的相片挑選工具,取得要上送的照片URI後,轉換成InputStream上送至服務。

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
public interface PhotoAPI {
@Headers({
"Content-type: application/octet-stream",
"X-Goog-Upload-Protocol: raw"
})
@POST("uploads")
Call<ResponseBody> uploadMediaItem(@Header("X-Goog-Upload-Content-Type") String mimeType, @Body RequestBody file);
}

public class MainActivity extends AppCompatActivity {
private ActivityResultLauncher<PickVisualMediaRequest> pickMultipleMedia =
registerForActivityResult(new ActivityResultContracts.PickMultipleVisualMedia(50), uris -> {
if (uris.isEmpty()) {
Log.d("Mike", "No media selected");
return;
}
Log.d("Mike", "Number of items selected: " + uris.size());
for (Uri uri : uris) {
ContentResolver contentResolver = getContentResolver();
try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
if (cursor.moveToFirst()) {
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME));
uploadMediaItem(displayName, contentResolver.getType(uri), contentResolver.openInputStream(uri));
}
} catch (Exception e) {
Log.e("Mike", e.getMessage(), e);
}
}
});

public void onUploadClick() {
pickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
.build());
}

public void uploadMediaItem(String fileName, String mimeType, InputStream in) {
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
.addInterceptor(new TokenInterceptor());
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://photoslibrary.googleapis.com/v1/")
.client(httpBuilder.build())
.addConverterFactory(GsonConverterFactory.create())
.build();

RequestBody body = new RequestBody() {
@Nullable
@Override
public MediaType contentType() {
return MediaType.parse(mimeType);
}

@Override
public void writeTo(BufferedSink sink) throws IOException {
try (Source source = Okio.source(in)) {
sink.writeAll(source);
}
}
};

retrofit.create(PhotoAPI.class).uploadMediaItem(mimeType, body)
.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {

}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {

}
});
}
}

參考資料

  1. https://developers.google.com/photos/library/guides/upload-media?hl=en#uploading-bytes
  2. https://developer.android.com/training/data-storage/shared/photopicker?hl=zh-tw

上送檔案(body為form-data)

以工作上遇到的日誌上送為例。

1
2
3
4
public interface CustomAPI {
@POST("Upload")
Call<ResponseBody> uploadLog(@Body RequestBody body);
}
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
public void uploadLog(String date, File zipFile) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://xxx/")
.addConverterFactory(GsonConverterFactory.create())
.build();

final RequestBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("date", date)
.addFormDataPart("log", zipFile.getName(), RequestBody.create(null, zipFile))
.build();

retrofit.create(CustomAPI.class).uploadLog(body)
.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {

}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {

}
});
}

串接Google API遇到java.lang.IllegalArgumentException: Malformed URL

以Google Photos API的mediaItems:batchCreate為例。因其使用gRPC轉碼語法,若直接使用會有問題,endpoint開頭加上./即可。

1
2
3
4
public interface PhotoAPI {
@POST("./mediaItems:batchCreate")
Call<BatchCreateMediaItemsResponse> batchCreateMediaItems(@Body BatchCreateMediaItemsRequest request);
}

參考資料

  1. https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate?hl=zh-tw
  2. https://stackoverflow.com/questions/54406788/retrofit-how-to-use-colon-in-url