Android:Retrofit2.0ではてなAPIとおしゃべりしてみた
Androidでネットワーク通信するアプリを作る際、少し前まではVolleyを使うのがイケてるAndroiderの嗜みだったようですが、最近ではどうやらRetrofitがその地位を奪いつつあるようです。
ということで遅ればせながらイケてるAndroider目指してRetrofitをお触りしてみます。
Retrofitとは
RetrofitはSquareが開発しているネットワークアクセスライブラリです。特徴的なのはサーバ側のAPIをインタフェースとして定義することで、API呼び出しの実装とAPI定義を分離しコードの見通しを簡潔に保つことができるという点だと思います。実装とAPI定義が分離しているため、後からAPIを追加することも容易となり保守性の高い設計ができそうです。
さらにAPI部分をインタフェース化することが必須なので、DIと非常に相性が良く容易にモックと差し替えられるので非常にテスタビリティが高いと言えます*1。このあたり今時っぽい感じがします。
ちなみに割りと最近バージョンが2.x系になったそうで、1.x系とは書き方が結構違うようなのでGoogle先生に聞く時はどのバージョンの話なのかに注意が必要です。
導入方法
導入はGradleファイルに1行書くだけです。2016/04/10時点での最新バージョンは2.0.1です。
compile 'com.squareup.retrofit2:retrofit:2.0.1'
ちなみに当然ながらインターネットのパーミッションが必要なのでマニフェストに追加しましょう。
<uses-permission android:name="android.permission.INTERNET"/>
インタフェースを定義する
無事に導入できたら呼び出し対象のAPIインタフェースを定義します。Retrofitの使い方を検索するとなぜかほとんど天気予報APIのサンプルばっかりだったので、今回ははてなAPIを使ってみました。
public interface HatenaApiInterface { String END_POINT = "http://b.hatena.ne.jp"; String TARGET_URL = "http://b.hatena.ne.jp/ctop/it"; @GET("/entry/jsonlite/") Call<BookmarkContainer> getBookmarksWithUrl(@Query("url") String target); }
各メソッドはアノテーションでhttpのアクセスメソッドを指定できます。カッコ内はベースURL以降のAPIのURLを指定します。ベースURLはAPIをコールする準備段階でセットしますが、上記の例ではアクセスURLはhttp://b.hatena.ne.jp/entry/jsonlite/?url=(実行時に指定)
となります。
メソッドの引数@Query("url") String target
は、引数のtargetをアクセス時のクエリパラメータに付与するという指定です。
戻り値のCall<T>
で任意の型T
を戻り値に指定できます。RetrofitではCallオブジェクトが1回のAPIコールに対応します。Call<T>.execute()
、Call<T>.enqueue(Callback<T>)
を実行することで、それぞれ同期・非同期のAPI呼び出しが実行できます。非同期実行の場合はenqueueで指定したコールバックでAPI実行結果をハンドリングできます。
データクラスを定義する
次にAPI実行後の戻り値を受けるためのデータクラスを定義します。複雑なモデルを定義することもできるみたいですが、POJO(Plain Old Java Object)*2なクラスにするとGsonConverterなどでサクッと変換してくれるようなので今回はその方向で行きます。
jsonschema2pojoというサイトでJSONオブジェクトからPOJOなオブジェクトを作成できるので、変換してクラスを追加します。別にレスポンスがJSONじゃなくてもいいみたいですが、XMLとかを扱おうとするとConverterを自作して自分でパースするとかいうめんどくさい作業が必要っぽいので今回はJSONでレスポンスを返してくれるAPIを使います。
とりあえず今回はデータクラスとして以下のクラスを作成しました。
public class BookmarkContainer { @Expose private List<Bookmark> bookmarks = new ArrayList<Bookmark>(); @Expose private Integer count; @Expose private Integer eid; @Expose private String entryUrl; @Expose private String screenshot; @Expose private String title; @Expose private String url; // 略 }
public class Bookmark { @Expose private String comment; @Expose private List<Object> tags = new ArrayList<>(); @Expose private String timestamp; @Expose private String user; // 略 }
POJOなクラスなのでメンバとセッタ/ゲッタがあるだけの単純なものです。注意点としては各メンバに@Expose
アノテーションを付けないとAPI実行時にGsonConverterで処理できないというようなエラーが出てアプリが落ちます。原因はよくわかりませんでしたがGsonを使う人にとってはアノテーション付けるのが常識?なんでしょうか。
API実行
ここまででAPI実行の準備ができたので実際にAPI呼び出し部分を作ってみます。基本的な流れは以下のようになります。
- Retrofitを設定する
- APIをインスタンス化する
- リクエストを1個ずつインスタンス化する(Callクラス)
- Callを実行する
- レスポンスを処理する
以下で1つずつ見ていきます。
Retrofitを設定する
Retrofit.Builderを通してRetrofitインスタンスを作成します。baseUrl
でベースURLを指定しaddConverterFactory
でレスポンスを処理するConverterを指定します。他にも色々あるみたいですがとりあえず今回はこれだけ。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(HatenaApiInterface.END_POINT)
.addConverterFactory(GsonConverterFactory.create())
.build();
ちなみにRetrofit1.9まではGsonConverterはRetrofitに内蔵されてたそうですが、2.0からは分けられているらしいので使う場合は手動で追加してやる必要があります。Gradleファイルに以下を追加しましょう。
compile 'com.squareup.retrofit2:converter-gson:2.0.0' compile 'com.google.code.gson:gson:2.6.2'
APIをインスタンス化する
上記で作成したRetrofitインスタンスから呼び出し対象のAPIをインスタンス化します。ここで呼び出し対象クラスを指定する時に、本番用と同じインタフェースを実装したテストクラスを指定することで、実行インスタンスの差し替えが容易に行なえます。
HatenaApiInterface service = retrofit.create(HatenaApiInterface.class);
リクエストを1個ずつインスタンス化する
作成したAPIインスタンスのメソッドを実行すると、API実行クラスがインスタンス化されます。この時点ではまだ実際のAPIコールは実行されません。
Call<BookmarkContainer> call = service.getBookmarksWithUrl(HatenaApiInterface.TARGET_URL);
Callを実行する
Callは同期/非同期のどちらでも実行できます。今回は非同期で実行してみます。引数のCallback<BookmarkContainer>
でレスポンスを処理します。
call.enqueue(new Callback<BookmarkContainer>() { // 略 }
レスポンスを処理する
リクエスト成功時にはonResponse
、失敗時にはonFailure
が実行されます。onResponse
のResponse
クラスにレスポンスが入っていてresponse.body()
で指定したConverterで変換した結果(ここではBookmarkContainer
)が取得できます。
後は煮るなり焼くなり好きにしましょう。今回ははてなブックマークのテクノロジーカテゴリトップについているブックマークのコメントをListViewに表示するという特に意味のないアプリです。
call.enqueue(new Callback<BookmarkContainer>() { @Override public void onResponse(Call<BookmarkContainer> call, Response<BookmarkContainer> response) { Log.d("AAA", "Successed to request"); BookmarkContainer container = response.body(); List<Bookmark> bookmarks = container.getBookmarks(); ArrayAdapter<String> adapter = new ArrayAdapter<>(MainActivity.this, android.R.layout.simple_list_item_1); for (Bookmark b : bookmarks) { if (b.getComment().length() > 0) adapter.add(b.getComment()); } mListView.setAdapter(adapter); } @Override public void onFailure(Call<BookmarkContainer> call, Throwable t) { Log.d("AAA", "Failed to request"); } });
アプリ起動後はこんな感じになります。
無事にリストが表示されていれば万事オッケーです。おめでとうございます。
所感
初めて触ってみたので実行するまでの流れとかレスポンスの処理とかがイマイチつかめず色々とハマった感は否めないが、一度慣れてしまえばコードの見通しも良いしテスタビリティも非常に高そうなのでかなり扱いやすそうな感じがした。
ただし現在は1.x系から2.x系の過渡期かつ日本語情報もまだまだ不足している(特に応用的なことはほとんど見つからない)感じ。後JSONを扱うにはすごい便利だけどRSSのフィード処理したいとかXML使いたいとかなると結構大変そう*3
サンプルコード
サンプル置いておきますね。