【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ

前書き

最近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#encodedtrueを渡してあげましょう。

クエリ

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@QueryMapencodedtrueを指定することで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でも同じだと思います。

パラメータとして渡すにはBodyField(もしくはFieldMapアノテーションを使います。

key/value形式に自動で変換して欲しい場合は@FormUrlEncoded@Filedを使います。

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

@BodyはそのまんまBody部に値をぶちこみます。ただ、Converter(後述)を指定することで独自の型でもいい感じに変換してくれます。*3

また、@FormUrlEncodedは強制的にUTF-8エンコードするので、他の文字コードでPOSTしなきゃいけないような場合も@Bodyを使わざるを得ないのかもしれません。(未検証)

@POST("users/new")
Call<User> createUser(@Body User user);

また、@FormUrlEncodedなデータをPOSTするときにデフォルト値を設定したいことが結構あるんですが、現在開発中なので、@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;
}
MultipartなデータをPOST

ちゃんとMultipartと言うアノテーションがあります。

渡す値はPart(もしくはPartMapアノテーションで指定します。

@Multipart
@PUT("user/photo")
Call<User> updateUser(@Part("photo") RequestBody photo, @Part("description") RequestBody description);

渡せる型はデフォルトだとokhttp3.MultipartBody.Partokhttp3.RequestBodyで固定されているようです。もちろん対応するConverterがあれば独自の型でもOKです。*4

なお、@Multipart@FormUrlEncodedは併用できません。Content-Typeが矛盾するからです。*5

ヘッダの指定

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つだけです。

okhttp3.Responseも色々さわりたい*6場合は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を使用するのが安全です。

JavaNetCookieJarCookieCookieHandler(=CookieManager)で管理するので、他のHTTPクライアントや、AndroidであればWebViewCookieを共有しやすくもなります。

ちなみにokhttp本体とは別モジュール*7なので、依存関係に追記が必要なことを忘れないでおきましょう。

dependencies {
    compile "com.squareup.okhttp3:okhttp-urlconnection:3.2.0"
}

今回は詳しく説明しませんが、同じくokhttp-urlconnection内にあるJavaNetAuthenticatorも中々便利そうなので、Retrofit or okhttpでプロキシ認証が必要になったら参考にしたり流用したりしましょう。

レスポンスのCall Adapter

Retrofit 2.xは、デフォルトのままだとレスポンスを必ずCall<T>でラップするようになっています。*8

Call<T>には同期的に実行するexecuteと非同期的に実行するenqueueの2つのメソッドが用意されています。

enqueueで渡すCallback<T>は確かに必要最低限の機能が用意されていますが、Promiseパターンが使えないので、複数APIを組み合わせて実行しようとするとJavaScriptもびっくりなコールバック地獄になります。

それは面倒だよね、ってことで、もっと便利なものにラップできるよう、Call Adapterが用意されています。

ライブラリ ラップする型 モジュール
RxJava ObservableSingle com.squareup.retrofit2:adapter-rxjava
Guava ListenableFuture com.squareup.retrofit2:adapter-guava
Java 8 CompleteableFuture com.squareup.retrofit2:adapter-java8

実際にどのCall Adapterを使うかはRetrofit.Builder#addCallAdapterFactoryで指定します。

RxJavaのAdapterを指定するならこんな感じです。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com")
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();

なお、RxJavaCallAdapterFactoryにはRxJavaCallAdapterFactory#createWithScheduler(Scheduler)と言うメソッドも用意されています。ここで指定しておけば生成されるObservableに対して自動でsubscribeOnしてくれるので、上手く活用しましょう。*9

レスポンスの自動型変換

HTTP通信によって取得した内容(Entity)は事前にConverterを指定しておくことで自動で型変換を行うことができます。*10

公式に用意されているものは以下の通りです。

ライブラリ モジュール
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*11 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を指定していても使用できます*12

また、戻り値の型をokhttp3.ResponseBodyにすることで、生のEntityやBodyを取得することもできます。(ex. Call<ResponseBody>

こちらもConverterに一切依存しないので、「欲しいConverterが用意されてないけど自力で実装するのダルい」「型変換のところでエラーが出るから値を確認しつつデバッグしたい」などの場合に使いましょう。*13

まとめ

またなんか無駄に濃密な記事になってしまいました。

*1:それ以外の型を指定すると「HEAD method must use Void as response type」と怒られる

*2:@QueryMapで同じようなことができるかどうかは未検証。Keyが同じになっちゃうから難しいような…?

*3:Retrofit 1.xでは勝手にJSON形式にしていたようだが、今はちゃんとConverterを指定してやる必要がある。

*4:Introductionで紹介されているConverterにはそれ用のものがないので、余程のことがないならMultipartBody.PartかRequestBodyでいい気がする。

*5:と、そんな回答がされている。まぁ、サーバ側の実装が悪いとしか言いようがないっちゃないよね。

*6:本来返されるレスポンスの内容をここで書き換えたりするのは行儀が悪い(と言うか、RetrofitにおいてそれはConverterのお仕事である)ので、各情報の取得だけにとどめておいた方が無難。

*7:なんでやねん。

*8:1.xでは違ったらしいけど割愛。

*9:特にAndroidでは「呼び出し元でsubscribeOnし忘れた」のようなしょうもない事故を防止するためになるべくこっちを使うべき。

*10:レスポンスだけでなく、リクエストで使う引数の型もConverterで変換できる。

*11:Wikiに載っている内容は誤っているので注意

*12:もちろん一つもConverterを指定していなくても使用可能。

*13:Converter.Factoryの実装はどうしても「どんな型が指定されても大丈夫!」って感じにせざるを得ないので、しょうもないConverterを大量生産するよりはコールバック内でそれぞれ型変換した方がまだマシな気がする。(個人の感想です。)