retrofit2での通信エラー処理
通信していると、エラー処理をする必要が出てくる。retrofit2の場合、callbackにonFailureというメソッドがあるけど、これは基本的に電波状態が良くなくてそもそもリクエストを送れなかったときとかに呼ばれる。なので、4xx系エラーとか5xx系エラーの処理はこのonFailureではできない。ステータスコードを確認するにはonResponseの中でisSuccessful()とか使わないといけない。が、onResponseに来た時点で、jsonはcallbackを定義した時のインスタンスに変換されちゃっている。
Call<User> call = service.getUser(); call.enqueue(new Callback<User>() { @Override public void onResponse(Call<User> call, Response<User> response) { if (response.isSuccessful()) { // do something } else { // 通信エラー } } });
こういう場合、onResponseの中ではjsonの結果はステータスコードによらず、Userオブジェクトインスタンスに変換されている。jacksonなどをconverterに使っているならJsonIgnoreProperties(ignoreUnknown = true)とかのアノテーション指定してエラー内容を無視するとか、Userインスタンスにエラー時のエラー内容を格納するプロパティ用意しとけばいいのかもしれないけど、それも面倒だ。通信で変換するクラス全てにエラー用フィールドを入れたくはない。
で、こういう時にもっと汎用的なエラーインスタンスを作りたい。isSuccessful()でelse節に飛んだ時、UserインスタンスじゃなくてAPIErrorクラスインスタンスとかもっと汎用的に使えるオブジェクトに変換して欲しい。そういう場合の手法。
エラーエンティティ
何にしてもUserエンティティのような汎用的エラーエンティティが必要になるので、それをまず定義する。
public class APIError { private int statusCode; private String message; public APIError() { } public int status() { return statusCode; } public String message() { return message; } }
簡単にエラーのステータスコードと、エラー内容文字列が取れるようにしているだけ。
エラーハンドラ
で、あとはそのrestfitのresponseオブジェクトをエラーエンティティに変換するものが必要になる。
public class ErrorUtils { public static APIError parseError(Response<?> response) { Converter<ResponseBody, APIError> converter = ServiceGenerator.retrofit() .responseBodyConverter(APIError.class, new Annotation[0]); APIError error; try { error = converter.convert(response.errorBody()); } catch (IOException e) { return new APIError(); } return error; } }
response.errorBody()でエラー内容を取得できるので、それをresponseBodyConverterで変換。responseBodyConverterの第一引数は変換結果としたいエラーエンティティのクラス。第二引数はちょっと調べてみたけどよくわからなかった。でもnew Annotation[0]のままで一切変えずともうまく行った。
というわけで、あとはこのstaticなメソッドをonResponseの中で呼び出してやればいい。
参考:
retrofitでマルチパートなものをPOSTする
retrofitはHTTPクライアントだけあって当然formの要素や画像のようなマルチパートなものもPOSTできるのだけど、その方法があんまり見つからなかったので覚書。見つからなかったというよりか、現状の最新安定バージョンの2.0でのPOSTの方法と過去のバージョン1.9での方法がかなり違う上に1.9での情報ばっかりがあふれていたので見つけにくかった…というだけだったのだけど。
概要
1.9ではTypedFileというクラスを使ってそれに画像なり何なりをいれてPOSTするのだけど、2.0ではRequestBodyにPOSTしたい要素を入れるようにする。 インターフェース部分は以下のように。@Multipartアノテーションと@Partアノテーションを使う。@Partアノテーションをつけた引数としてRequestBodyを指定する。
public interface FileUploadService { @Multipart @POST("/upload") Call<String> upload( @Part("file") RequestBody file ); }
で、使う場合は以下のようにする。RequestBodyのインスタンスを生成する時に、MediaTypeを指定する必要がある。
FileUploadService service = ServiceGenerator.createService(FileUploadService.class); File file = new File("path/to/your/file"); RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file); Call<String> call = service.upload(requestBody); call.enqueue(new Callback<String>() { //... });
たくさんの要素をPOSTしたい
formと言っても要素は複数あるとか、画像1枚だけじゃなくて複数枚POSTしたいという場合はPartMapを使う。 @Partアノテーションでなく、@PartMapアノテーションを使う。そして引数はその名の通りMap。
public interface FileUploadService { @Multipart @POST("/upload") Call<String> upload( @PartMap Map<String, RequestBody> params ); }
FileUploadService service = ServiceGenerator.createService(FileUploadService.class); Map<String, RequestBody> requestParams = new HashMap<>(); File file1 = new File("path/to/your/file1"); RequestBody requestBody1 = RequestBody.create(MediaType.parse("multipart/form-data"), file1); File file1 = new File("path/to/your/file2"); RequestBody requestBody2 = RequestBody.create(MediaType.parse("multipart/form-data"), file2); requestParams.put("files[]", requestBody1); requestParams.put("files[]", requestBody2); Call<String> call = service.upload(requestParams); call.enqueue(new Callback<String>() { //... });
という感じで本当にMapライクに使えるので便利
@Bodyと併用は出来ない
以下のようなインターフェースを書いてたらエラーが出た。
public interface FileUploadService { @Multipart @POST("/upload") Call<String> upload( @Body ItemEntity item, @PartMap Map<String, RequestBody> params ); }
ItemEntityはjavaのオブジェクト<=>jsonの変換をしている。json<=>javaオブジェクトのコンバーターが絡む@Bodyとコンバートをしない@Partおよび@PartMapではうまくいかないみたい。なのでどっちかにする必要がある。画像をあげる必要があるなら@Partを素直に使ったほうが楽かもしれない。@Bodyだとfileはそのままjson変換されたらおかしくなるためbase64エンコードするとか色々面倒事があるので…
参考 :
Retrofit 2 — How to Upload Files to Server
How to Upload a File using Retrofit 2.0 · Issue #1063 · square/retrofit · GitHub
retrofitでHeaderを変える
OkHttp側のインターセプターでヘッダーの値を変えることもできるけど、retrofitでもヘッダーの設定が出来る。APIごとにヘッダを変えるだけでなく、ヘッダーの内容に変数を含めて動的にヘッダーの値を変えることも出来る。
以下、バージョンはretrofit2.0。
APIごとにヘッダーを設定
public interface UserService { @Headers("Cache-Control: max-age=640000") @GET("/tasks") Call<List<Task>> getTasks(); }
こういう感じで@HeaderアノテーションをつければOK。retrofitの場合はメソッドというかAPIごとの設定になるのでOkHttpでの方法よりも小回りがきく。
動的にヘッダーを変える
ヘッダーの内容に変数を渡したい時もretrofitがなんとかしてくれる。
public interface UserService { @GET("/tasks") Call<List<Task>> getTasks(@Header("Content-Range") String contentRange); }
これでgetTasksメソッドを呼ぶときにcontentRangeという文字列引数を渡してあげれば、その文字列がそのままヘッダーの値として入るようになる。 常時つけるような静的なヘッダーはOkHttpのインターセプターで、OkHttp側でやるのは大仰過ぎるとか動的に値をコントロールしたいのであればretrofit側で行うというほうがよさそうだ。
参考:
retrofitでAPIを楽に使う
androidから何かサーバ側のAPIを叩く場合、色々と用意しなければならない。OkHttpなどのHTTP通信を行うインスタンス、通信は基本非同期処理なのでコールバックを渡すためのラッパー、それにAPIのURLとパラメータのチェック、APIから返ってきた値をコンバートするためのものなど…それらの面倒事を一手に引き受けてくれるのがretrofitだ。
概要
retrofitはHTTPクライアント。javaのインターフェース形式でAPIを定義出来る。こんな風に。
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
/users/{user}/reposというパスに対してGETアクセスをするというメソッドがこれだけで定義できる。{user}は可変のユーザー名。userに入る引数とともにこのlistReposというメソッドを呼んであげればそれでHTTPGETが出来る。返り値はRepoクラスのインスタンスのListとなる。あとはbuilderを使ってこのインターフェースを実装したインスタンスを作ればオールOK。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .build(); GitHubService service = retrofit.create(GitHubService.class); Call<List<Repo>> repos = service.listRepos("octocat");
当然GETだけでなくPOSTやPUTやDELETEも可能。formEncordedなものや画像データ、マルチパートも投げることができる。 内部的にはOkHttpやVolleyなどをクライアントとして利用することができる。なのでOkHttpに噛ませたインターセプターなどともバッティングすることなく利用することができる。 また、レスポンスはjacksonやgsonなどのコンバータがサポートされているので、サポートされているもののうち好きなコンバータを使えばレスポンスjson=>javaオブジェクトへの変換も勝手にやってくれる。
導入
android studioでgradleを使っているなら以下をいれてsyncすればOK。
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta3'
バージョンは2をしていしたほうがいいだろう。それまではバージョン1.9がメインだったものの、夏ごろにメジャーバージョンがあがった。で、メジャーバージョンがあがったのにつれてインターフェースの宣言の仕方などがかなり変わったようだし、今のところは2.0に関しての情報も十分にあるので1.9を選ぶ必要はない。以降はバージョン2.0での使い方を説明する。
jsonのコンバーターを指定する場合はそれ用のライブラリも必要となることに注意。自分はjacksonを使っているので、以下も一緒にbuild.gradleに書き加えてある。
compile 'com.squareup.retrofit:converter-jackson:2.0.0-beta2'
使ってみよう
使う前にはまずレスポンスを格納するEntityクラスが必要なのでそれを作っておく。jacksonをつかっているので、jackson形式でのEntityを定義している。ここは適宜gsonなどに変えればいい。
public class Repo { public int id; public String name; public String url; public boolean private; }
そして重複するがインターフェースの宣言。
public interface GitHubService { @GET("users/{user}/repos") Call<List<Repo>> listRepos(@Path("user") String user); }
あとはこれを呼ぶ。
public class GithubActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_github); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .build(); GitHubService service = retrofit.create(GitHubService.class); Call<List<Repo>> call = service.listRepos("shim0mura"); call.enqueue(new Callback<List<Repo>>() { @Override public void onResponse(Response<List<Repo>> response) { if (response.isSuccess()) { List<Repo> result = response.body(); } } @Override public void onFailure(Throwable t) { } }); } }
serviceという変数にretrofitのインターフェースを実装したインスタンスをいれる。そしてそれから定義したメソッドを呼ぶ。そのメソッドの返り値にenqueueしたところで通信を始める。enqueueメソッドの引数はコールバック。通信が終了したところでコールバック内のメソッドが実行される。基本的に20x系のレスポンスでも50xや40xなどのエラー系のレスポンスでも全部onResponseが呼び出される。なので20x系の成功時のレスポンスを判定したいときはisSuccessメソッドで判定を行う。あとはonResponseの仮引数、この場合はresponseというインスタンスのbodyメソッドを呼べば、jsonなどの生のレスポンスは先ほど定義したEntityクラスインスタンスに勝手にコンバートしてくれる。楽ちん。
enqueueを使わずにcall.execute()とすれば非同期でない同期的な通信もできるらしい。使ったこと無いけど…
POSTも出来る
GET以外については以下のようにメソッドを定義する。
@POST("/user") Call<User> createUser( @Body HashMap<String, User>user );
@Bodyアノテーションでpostしたいオブジェクトを指定すればいい。呼び出し時も
new HashMap<String, User> postUser = new HashMap<>(); User user = new User(); user.name = "shim0mura"; user.job ="unemployed"; postUser.put("user", user); Call<User> call = service.createUser(postUser);
とGETとほとんど変わることなく呼び出せる。enqueueなどのコールバック処理も全く同じ。インターフェース側だけ変えればいいだけなのである。 ちなみにわざわざHashMapを渡さず
@POST("/user") Call<User> createUser( @Body User user );
と直にEntityを渡す事もできる。というかそっちの方が普通な使い方。じゃあ何故HashMapなんか使ったのかというと、サーバ側でparams[:user][:name]というrailsっぽい形式で取得したかったため。後者のような普通なpostだとparams[:CreateUser]の中にnameやjobが入っているという形式になってしまう。そのままだとどうやらメソッド名(しかもアッパーキャメル)で内容がpostされてしまう。HashMapを使えば、hashのキーがpost内容のキーになるのでrails使いには楽。
OkHttpを使う
更に、retrofitの内部のHTTPクライアントとしてOkHttpやVolleyなどを使うこともできる。 使い方というか指定の仕方は簡単。
OkHttpClient client = new OkHttpClient(); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .client(client) .build();
これだけ。buildの際にclientメソッドを呼び出し、その引数としてOkHttpのインスタンスを入れればいいだけ。なのでそのインスタンスをclientに入れる前にインターセプターを噛ましていても問題ない。
OkHttpClient client = new OkHttpClient(); client.interceptors().add(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(chain.request()); // Do anything with response here return response; } }); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.github.com") .client(client) .build();
これでちゃんとインターセプターは動作する。
コンバータの指定
更に更にコンバータもHTTPクライアントと同じようにして指定することができる。
Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://api.nuuneoi.com/base/") .addConverterFactory(JacksonConverterFactory.create()) .build();
これだけ。コンバータをgsonに変えたくなったら、GsonConverterFactory.create()に変えてやればいい。(もちろんbuilde.gradleでgsonコンバータを加えている前提)
という感じで自前の面倒なコンバータやAPIラッパーを作らなくてもretrofitさえあれば大体のことはまかなえる。素敵。