【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ
2018/11/29 追記
前書き
最近Retrofitを使うことがあったんですが、イントロダクションをちょろっと読んだぐらいだと「え、そーなの?」と思うような事象に何度も見舞われたので、メモしておきます。
API の設定
HTTP メソッドやパス、クエリやパラメータなどを適当に作ったインターフェースとアノテーションで表現します。
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
ベースとなる URL はRetrofit.Builder#baseUrl
で渡すので、相対パスを書いておけば OK です。
アノテーションだけでどうにでもなってしまうので、色んなサービスの API を叩きたい時に共通の型がなくて却って使いにくいこともたまーにありますが、その辺は自分で共通の型を作ればいいだけの話です。
HTTP Methods
HTTP のメソッドに対応するアノテーションは以下のものが用意されています。必要十分って感じですね。
PATCH
ってDELETE
以上に見たことないけど、意識の高い API 設計者は使うんでしょうか。
独自のメソッドを用意している意識が高すぎる API に対してはHTTP#method
に渡しましょう。
あと、微妙にハマりポイントなんですが、HEAD
を指定したメソッドの返り値の型は必ずCall<Void>
にする必要があります。*1
普通にヘッダだけ欲しいんだけどどうしたらええの?と言う質問が Issue にあるんですが、ガン無視されています。私はもう面倒なので GET メソッド投げるようにしてしまいました。(怒)
ルーティング
@GET("users/{user}/repos")
の{user}
部分をメソッドの引数で渡すことができます。
対応する引数にPath
アノテーションをつけてあげれば OK です。
マルチバイト文字とか記号とかをうにゃうにゃしたい時はPath#encoded
にtrue
を渡してあげましょう。
クエリ
Query
アノテーションを指定してあげればよしなにやってくれます。
@GET("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
optional なパラメータが多い時はQueryMap
アノテーションを使うと便利です。
@GET("group/{id}/users") Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);
@Query
も@QueryMap
もencoded
にtrue
を指定することで URL エンコードできるので、特に理由がないならやっておいた方が色々安全だと思います。
また、うっかりミスでこういうことをやってしまうことがあるんですが、これは怒られます。
// クエリ部分にPathで値を渡すのは無理 // 渡せるようにしても良い気がするけどね。 @GET("group/{id}/users?sort={sort}") Call<List<User>> groupList(@Path("id") int groupId, @Path("sort") String sort);
クエリ応用編
@Query
を指定する引数を配列 / 可変長引数にすることで、同じクエリを複数指定することができます。
@GET("/list") Call<ResponseBody> list(@Query("category") String... category);
例えば、service.list("hoge", "fuga")
と呼び出したら、/list?category=hoge&category=&fuga
になります。*2
また、特定のクエリを固定値で指定しておく場合はこのように書けば OK です。うまいこと後ろにクエリを足してくれます。
@GET("group/{id}/users?sort=desc") Call<List<User>> groupList(@Path("id") int groupId, @Query("filter") String filter);
クエリ更に応用編(外法)
GET
だけでなくどのメソッドでもいいんですが、Url
アノテーションを引数として渡すことで API のパス自体を動的に変更できます。
public interface GitHubService { @GET Call<List<Repo>> listRepos(@Url String url); }
極端な話、この@Url
にパスもクエリもぶち込んでしまえば動いてくれます。型安全とは何だったのか。
POST のパラメータ
多分、PUT
でも同じだと思います。
パラメータとして渡すにはBody
かField
(もしくはFieldMap
)アノテーションを使います。
application/x-www-form-urlencoded
形式での POST
application/x-www-form-urlencoded
(key=value) 形式に自動で変換して欲しい場合は@FormUrlEncoded
+@Filed
を使います。
@FormUrlEncoded @POST("user/edit") Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
デフォルト値を設定する方法は現在開発中なので、@FieldMap
と適当なデフォルト値を持つMap<String, String>
を作るメソッドを併用するぐらいしか対処法がありません。
@FormUrlEncoded @POST("user/edit") // Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last); Call<User> updateUser(@FieldMap Map<String, String> names);
public static Map<String, String> createFullName(String first) { final Map<String, String> param = new HashMap<>(); param.put("first_name", first); param.put("last_name", "hogehoge"); return param; }
それ以外の形式での POST
@Body
を使うとそのまんま Body 部に値をぶちこみます。application/json
などを渡したい場合はこちらを使いましょう。
ちなみにConverter
(後述)を指定することで独自の型でもいい感じに変換してくれます。*3
@POST("users/new") Call<User> createUser(@Body User user);
UTF-8 以外でエンコードしたapplication/x-www-form-urlencoded
@FormUrlEncoded
はパラメータを強制的に UTF-8 でエンコードしてしまいます。この文字コードを変えることはできません。*4
どうしても他の文字コードで POST する必要がある場合も@Body
を使う必要があります。
Converter
を自作してもいいですが、スカラー値を扱う Converterが公式で用意されているので、自分でapplication/x-www-form-urlencoded
形式のString
を作ってしまうのが早いです。*5
// API定義 @POST //@FormUrlEncoded //Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last); Call<User> updateUser(@Body String body);
// 呼び出し方 String firstName = "山田"; String lastName = "太郎"; Function<String, String> encode = (s) -> { try { // java.net.URLEncoderでUTF-8以外の文字コードに変換 return URLEncoder.encode(s, "Shift-JIS"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } }; // 自分でapplication/x-www-form-urlencoded形式のStringを作る String body = String.format("%s=%s&%s=%s", encode.apply("first_name"), encode.apply(firstName), encode.apply("last_name"), encode.apply(lastName)); // StringをRequestBodyとして投げる Call<User> user = service.updateUser(body);
Multipart なデータを POST
渡す値はPart
(もしくはPartMap
)アノテーションで指定します。
@Multipart @PUT("user/photo") Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);
渡せる型はデフォルトだとokhttp3.MultipartBody.Part
かokhttp3.RequestBody
で固定されているようです。もちろん対応するConverter
があれば独自の型でも OK です。*6
なお、@Multipart
と@FormUrlEncoded
は併用できません。Content-Type
が矛盾するからです。*7
ヘッダの指定
Headers
アノテーションを指定しておくことで、事前にヘッダを作っておくことができます。
@Headers("Cache-Control: max-age=640000") @GET("widget/list") Call<List<Widget>> widgetList();
@Headers({ "Accept: application/vnd.github.v3.full+json", "User-Agent: Retrofit-Sample-App" }) @GET("users/{username}") Call<User> getUser(@Path("username") String username);
また、Header
アノテーションを引数に指定することで、動的にヘッダ部を変更することができます。
@GET("user") Call<User> getUser(@Header("Authorization") String authorization)
ただ、全 API 共通で指定したいヘッダはokhttp3.Interceptor
を作ったほうがいいです。(後述)
実際の通信部分の設定
Retrofit.Builder#client
で指定するokhttp3.OkHttpClient
が実際に HTTP 通信を行うクライアントになります。
どんな設定ができるかはokhttp3.OkHttpClient.Builder
のメソッドを見れば大体わかると思います。また、Wiki の Recipesもかなり参考になると思います。
Proxy や Timeout、Cookie / キャッシュの管理もこれでやることになるので、Retrofit を使う場合は必ず一読しておいたほうがいいです。
ちなみにOkHttp3 ではOkHttpClient
のコネクションプールを各インスタンス毎に持っているのでシングルトンで持っておくことが推奨されています。Interceptor
やタイムアウト値、リトライ回数なんかを API 別に設定したい場合はokhttp3.OkHttpClient#newBuilder
を使いましょう。
okhttp3.Interceptor
の設定
下手なコードを読むよりWikiを読んだほうがわかりやすいです。
okhttp3.Interceptor
を実装するにあたって覚えておくことは以下の 3 つだけです。
chain#request()
で発行する直前のokhttp3.Request
を取得okhttp3.Request#newBuilder()
でリクエストの上書きchain#proceed(Request)
でリクエストを発行
okhttp3.Response
も色々さわりたい*8場合はproceed
で取得した内容をごにょごにょしましょう。
単純に共通のヘッダを指定したいだけならこんな感じになります。複数のヘッダを指定したい場合はokhttp3.Headers
を使ったりしましょう。
public class CommonInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { return chain.proceed( chain.request().newBuilder() .header("User-Agent", "OkHttp Example") .build() ); } }
Cookie の設定
okhttp3.CookieJar
を適当に実装してあげましょう。
一ミリも頭を使わない実装をするとこんな感じになります。ただ、あまりにも Cookie の仕様をガン無視しているのでおすすめできません。
public class NoHeadCookieJar implements CookieJar { private final List<Cookie> saveCookies = new ArrayList<>(); @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) { for(Cookie c : cookies) { if(!saveCookies.contains(c)) saveCookies.add(c); } } @Override public List<Cookie> loadForRequest(HttpUrl url) { return saveCookies; } }
きちんと Cookie を管理するのであればJavaNetCookieJar
を使用するのが安全です。
JavaNetCookieJar
は Cookie をCookieHandler
(=CookieManager
)で管理するので、他の HTTP クライアントや、Android であればWebView
と Cookie を共有しやすくもなります。
ちなみにokhttp
本体とは別モジュール*9なので、依存関係に追記が必要なことを忘れないでおきましょう。
dependencies {
compile "com.squareup.okhttp3:okhttp-urlconnection:3.2.0"
}
今回は詳しく説明しませんが、同じくokhttp-urlconnection
内にあるJavaNetAuthenticator
も中々便利そうなので、Retrofit or okhttp でプロキシ認証が必要になったら参考にしたり流用したりしましょう。
レスポンスの Call Adapter
Retrofit 2.x は、デフォルトのままだとレスポンスを必ずCall<T>
でラップするようになっています。*10
Call<T>
には同期的に実行するexecute
と非同期的に実行するenqueue
の 2 つのメソッドが用意されています。
enqueue
で渡すCallback<T>
は確かに必要最低限の機能が用意されていますが、Promise パターンが使えないので、複数の API を組み合わせて実行しようとすると JavaScript もびっくりなコールバック地獄になります。
それは面倒だよね、ってことで、もっと便利なものにラップできるよう、Call Adapterが用意されています。
ライブラリ | ラップする型 | モジュール |
---|---|---|
RxJava 1.x | Observable / Single |
com.squareup.retrofit2:adapter-rxjava |
RxJava 2.x | Observable / Flowable / Single / Maybe / Completable |
com.squareup.retrofit2:adapter-rxjava2 |
Guava | ListenableFuture |
com.squareup.retrofit2:adapter-guava |
Java 8 | CompleteableFuture |
com.squareup.retrofit2:adapter-java8 |
実際にどの Call Adapter を使うかはRetrofit.Builder#addCallAdapterFactory
で指定します。
RxJava 1.x の Adapter を指定するならこんな感じです。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com") .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build();
なお、RxJavaCallAdapterFactory
にはRxJavaCallAdapterFactory#createWithScheduler(Scheduler)
と言うメソッドも用意されています。ここで指定しておけば生成されるObservable
に対して自動でsubscribeOn
してくれるので、上手く活用しましょう。*11
リクエスト・レスポンスの自動型変換
HTTP 通信によって取得した内容(Entity)は事前にConverter
を指定しておくことで自動で型変換を行うことができます。*12
公式に用意されているものは以下の通りです。
ライブラリ | モジュール |
---|---|
Gson | com.squareup.retrofit2:converter-gson |
Jackson | com.squareup.retrofit2:converter-jackson |
Moshi | com.squareup.retrofit2:converter-moshi |
Protobuf | com.squareup.retrofit2:converter-protobuf |
Wire | com.squareup.retrofit2:converter-wire |
Simple XML | com.squareup.retrofit2:converter-simplexml |
Scalars (primitives, boxed, and String) | com.squareup.retrofit2:converter-scalars |
使用する Converter はRetrofit.Builder#addConverterFactory
で指定することができます。Call Adapter と一緒ですね。
Gson の Converter を指定する場合はこんな感じになります。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .addConverterFactory(GsonConverterFactory.create()) .build();
HTTP 通信結果の取得
ステータスコードやヘッダといった情報をコールバックで取得したい場合は、返り値の型をResponse<T>
でラップしましょう。(ex. Call<Response<String>>
)
ちなみにResponse<T>
は Converter に一切依存しないので、どんな Converter を指定していても使用できます*13
また、戻り値の型をokhttp3.ResponseBody
にすることで、生の Entity や Body を取得することもできます。(ex. Call<ResponseBody>
)
こちらもConverter に一切依存しないので、「欲しい Converter が用意されてないけど自力で実装するのダルい」「型変換のところでエラーが出るから値を確認しつつデバッグしたい」などの場合に使いましょう。*14
まとめ
またなんか無駄に濃密な記事になってしまいました。
*1:それ以外の型を指定すると「HEAD method must use Void as response type」と怒られる
*2:@QueryMap で同じようなことができるかどうかは未検証。Key が同じになっちゃうから難しいような…?
*3:Retrofit 1.x では勝手に JSON 形式にしていたようだが、今はちゃんと Converter を指定してやる必要がある。
*4:現在の application/x-www-form-urlencoded の仕様的に仕方ないんですが…。
*5:Converter を自作しても結局同じようなことをする必要がある。
*6:Introductionで紹介されている Converter にはそれ用のものがないので、余程のことがないなら MultipartBody.Part か RequestBody でいい気がする。
*7:と、そんな回答がされている。まぁ、サーバ側の実装が悪いとしか言いようがないっちゃないよね。
*8:本来返されるレスポンスの内容をここで書き換えたりするのは行儀が悪い(と言うか、Retrofit においてそれは Converter のお仕事である)ので、各情報の取得だけにとどめておいた方が無難。
*9:なんでやねん。
*10:1.x では違ったらしいけど割愛。
*11:特に Android では「呼び出し元で subscribeOn し忘れた」のようなしょうもない事故を防止するためになるべくこっちを使うべき。
*12:レスポンスだけでなく、リクエストで使う引数の型も Converter で変換できる。
*13:もちろん一つも Converter を指定していなくても使用可能。
*14:Converter.Factoryの実装はどうしても「どんな型が指定されても大丈夫!」って感じにせざるを得ないので、しょうもない Converter を大量生産するよりはコールバック内でそれぞれ型変換した方がまだマシな気がする。(個人の感想です。)