紀錄開發上常遇到的問題,避免重複踩坑。
紀錄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) {
} }); } }
|
參考資料
- https://developers.google.com/photos/library/guides/upload-media?hl=en#uploading-bytes
- 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 Photos API的mediaItems:batchCreate為例。因其使用gRPC轉碼語法,若直接使用會有問題,endpoint開頭加上./即可。
1 2 3 4
| public interface PhotoAPI { @POST("./mediaItems:batchCreate") Call<BatchCreateMediaItemsResponse> batchCreateMediaItems(@Body BatchCreateMediaItemsRequest request); }
|
參考資料
- https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate?hl=zh-tw
- https://stackoverflow.com/questions/54406788/retrofit-how-to-use-colon-in-url