ottoのハマリポイント
イベントのsubscribeとpublishはottoを使っている。なんか今のところこれが情報多そうだったから。
が、つかっててハマるポイントもあったので覚書
UIへの変更は出来ない
他の遅延タスク系と同じで、UIへの変更はotto経由だとうまくいかない。なので、looperを噛ませないといけない。 Busインスタンスはシングルトンで他のところからも使い回すような形にすることが多いと思う。で、そのシングルトンでインスタンスを作る時にそのlooperを噛ませたラッパークラスを使う。
public class MainThreadBus extends Bus { private final Handler mHandler = new Handler(Looper.getMainLooper()); @Override public void post(final Object event) { if (Looper.myLooper() == Looper.getMainLooper()) { super.post(event); } else { mHandler.post(new Runnable() { @Override public void run() { MainThreadBus.super.post(event); } }); } } public class BusHolder { private static final Bus sBus = new MainThreadBus(); public static Bus getInstance() { return sBus; } }
これでBusHolder.getInstance().post()とかすればUIにも変更が加えられる。 参考: android - How to send event from Service to Activity with Otto event bus? - Stack Overflow
サブクラスでのsubscribeはできない。
ActivityでonResumeの中あたりでBusHolder.getInstance().register(this)とかして、@Subscribeアノテーションをつけたメソッドを作ってイベント発行しても、なぜかsubscribe側のメソッドが呼ばれない場合がある。なんでだろうと思って色々いじってみたけど、そのactivityが別アクティビティのスーパークラスだったのがいけなかった。インターフェースでもだめらしい。なので、@Subscribeなメソッドはサブクラス自体に書かないといけないっぽい。面倒…
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側で行うというほうがよさそうだ。
参考: