Retrofit2.0でホッテントリのRSSをパースしてみる
前回はRetrofitではてなAPIを叩いて取得したJSONをパースしてリスト表示してみました。今回はもう一歩進んで、はてぶホッテントリのRSSフィードを取得してリスト表示する、簡易ホッテントリリーダーを作ってみた話です。
最終的なサンプルは前回作成したプロジェクトをベースに改造したものです。
なおRetrofitの基本的な使い方は前回の記事を参照してください。
RSSをパースしたい!
前回はRetrofitの基本的な使い方を学ぶために、とりあえず適当なJSONを返してくるAPIを叩いて、パースした結果を表示するというサンプルを作ってみた。この時に使ったのがはてなの「指定したURLに付けられているはてなブックマーク情報を取得する」というAPIだったが、いかんせん取得できる情報がURLに付随するブックマークの情報なので、ブコメを表示するぐらいしか表示するものがなく実用上は特に意味のない感じの何かが出来上がった。
これではちょっと悲しいのでもう少し実用的なものにしたいと思って「カテゴリ別ホッテントリリーダー」的なものを作ろうかと思ったところ、ホッテントリのフィード取得APIはJSONではなくRSSを返してくるということがわかった*1。
RetrofitではJSONなら簡単に扱えるけどXMLを扱うのはちょっと難しいのかなーと思って前回は諦めたが、実はRetrofitにはSimpleXmlConverterというXMLをパースするためのConverterが指定できるということがわかったので実際に試してみた。
Simpleを使う
Simpleというのは元々Java用のXMLシリアライズ/デシリアライズライブラリで、XMLの構造を表現するPOJOなクラスを用意しておくだけでXML→POJO変換やPOJO→XML変換をいい感じに実行してくれるという代物らしい。要するにGsonのXML版と思っておけば良さそう*2。
で、前回使用したGsonConverterの代わりにSimpleXmlConverterというやつを使えば前回のJSONのパースと同じような感じでXML(今回はRSSのフィード)をパースできるようだ。
ということでSimpleXmlConverterを使うためにbuild.gradleに以下の記述を追加する。なおAndroid組み込みのライブラリといくつかコンフリクトするようなので、それらのライブラリを読み込まないように除外する必要がある模様。
android - Simple XML with Retrofit gradle issue - Stack Overflow
compile('com.squareup.retrofit2:converter-simplexml:2.0.0') { exclude module: 'stax' exclude module: 'stax-api' exclude module: 'xpp3' }
APIを追加する
まずは前回作成したAPIを表現するクラスHatenaApiInterface
に今回実行するためのAPIを追加する。
@GET("/hotentry/{category}") Call<Rdf> getHotentriesWithCategory(@Path("category") String category);
@Path
アノテーションは@GET
アノテーションで指定したパラメータ部分を置き換えるためのアノテーションで、実行時に引数に渡したcategory
の値が@GET("/hotentry/{category}")
の{category}
部分に代入される。
Rdf
クラスはRSSフィードのルートノードで、API実行時に取得したRSSフィードがRdfクラスに変換される。
RSSフィードのPOJOクラスを作成する
次にRSSフィードの構造を表現するPOJOクラスを作成していくが、その前にまずは変換対象のRSSを確認してみる。
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" xmlns:opensearch="http://a9.com/-/spec/opensearchrss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#" xmlns:media="http://search.yahoo.com/mrss"> <script/> <channel rdf:about="http://b.hatena.ne.jp/hotentry/it"> <title>はてなブックマーク - 人気エントリー - テクノロジー</title> <link>http://b.hatena.ne.jp/hotentry/it</link> <description>最近の人気エントリー - テクノロジー</description> <items> <rdf:Seq>...</rdf:Seq> </items> </channel> <item rdf:about="http://japanese.engadget.com/2016/04/15/rm-rf-qanda/"> <title>...</title> <link>...</link> <description>…</description> <content:encoded>...</content:encoded> <dc:date>2016-04-16T10:12:14+09:00</dc:date> <dc:subject>テクノロジー</dc:subject> <hatena:bookmarkcount>324</hatena:bookmarkcount> </item> <item rdf:about="http://www.mediainfo.biz/entry/media-create-new-5828">...</item> … </rdf:RDF>
RSSの構造自体は比較的単純で、ルートのRDF要素の下にChannelと複数のItemがあるだけだった。SimpleではPOJOを定義するのにいくつかのアノテーションを使うが、この指定が厄介で今回一番ハマりまくったところ。普通のXMLだったら特に難しいところはないが、今回のように<rdf:RDF 〜>
と要素名にコロンが入っている場合にどのように指定すればいいかが全然わからなかった。
ググりまくってもそれらしい情報が全然見つからず困ったが、Simpleの公式ページのTutorialをよく見てみると@Namespace
というアノテーションを指定すればいいっぽいということがわかった*3。ということで見よう見まねでPOJOクラスを定義してみたら以下のような感じになった。
要素名にコロンが入る要素は、Namespeceとプリフィックスを指定してやればちゃんとパースしてくれるようである。各アノテーションの簡単な説明は以下のとおり。
アノテーション名 | 使い方 |
---|---|
@Root | 子要素を持つ要素に指定する。必ずしもファイル全体のルートではないことに注意。strict = falseとすることで、メンバに定義していない要素は単に無視されるようになる。指定してない場合は実行時にエラーが発生する。 |
@NamespeceList | 使用する名前空間をリストで指定する。 |
@Namespace | 使用する名前空間を個別に指定する。prefixを指定することで、この名前空間の要素が適切に変換されるようになる。 |
@Element | 子要素を持たない要素。requiredを指定することができる。 |
@Attribute | 各要素の属性。requiredを指定することができる。 |
APIをコールする
APIを追加してPOJOを作成したら準備完了。ということで早速APIを実行してみる。今回ははてぶホッテントリのRSSフィードを取得してリスト表示後、各項目をタップするとブラウザを起動して該当ページを表示するという動作を実装してみた。ついでなのでブクマ数も表示している。
public class MainActivity extends AppCompatActivity { private HatenaApiInterface mApiInterface; private ListView mListView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.listView); Retrofit retrofit = new Retrofit.Builder() .baseUrl(HatenaApiInterface.END_POINT) // .addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(SimpleXmlConverterFactory.create()) // SimpleXmlConverterを指定 .build(); mApiInterface = retrofit.create(HatenaApiInterface.class); execGetHotentriesWithCategory(HatenaApiInterface.CATEGORY_IT); } // 略 }
addConverterFactory
でSimpleXmlConverterを指定すると、APIコール後のレスポンスを先ほど作成したPOJOクラスにパースすることができる。
execGetHotentriesWithCategory
は以下のようになっている。
private void execGetHotentriesWithCategory(String category) { Log.d("AAA", "execGetHotentriesWithCategory : category = " + category); Call<Rdf> call = mApiInterface.getHotentriesWithCategory(HatenaApiInterface.CATEGORY_IT); call.enqueue(new Callback<Rdf>() { @Override public void onResponse(Call<Rdf> call, Response<Rdf> response) { Log.d("AAA", "Successed to request"); Rdf rdf = response.body(); List<Item> items = rdf.getItemList(); BookmarkItemAdapter adapter = new BookmarkItemAdapter(MainActivity.this, R.layout.list_item, items); mListView.setAdapter(adapter); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Item item = (Item) parent.getItemAtPosition(position); // リストをクリックするとブラウザへ遷移させる Uri uri = Uri.parse(item.getLink()); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); } }); } @Override public void onFailure(Call<Rdf> call, Throwable t) { Log.d("AAA", "Failed to request : " + t.getCause() + ", " + t.getMessage()); t.printStackTrace(); } }); }
リクエストが成功すると前回JSONをパースした時と同様、request.body()
でCall<T>
に指定した型T
が取得できる。今回ははてぶホッテントリのRSSを表すRdf
インスタンスとなる。
後はAndroidでよくある感じのListView表示を実装してやれば以下のような簡易ホッテントリリーダの完成。
ListView用のカスタムアダプタは特に何の変哲もない実装。
private static class BookmarkItemAdapter extends ArrayAdapter<Item> { public BookmarkItemAdapter(Context context, int resource, List<Item> objects) { super(context, resource, objects); } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate(R.layout.list_item, null); } Item item = this.getItem(position); TextView title = (TextView) convertView.findViewById(R.id.title); title.setText(item.getTitle()); TextView count = (TextView) convertView.findViewById(R.id.count); count.setText(Integer.toString(item.getBookmarkCount())); return convertView; } }
リストアイテムのレイアウト。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="8dp" android:orientation="horizontal"> <TextView android:id="@+id/title" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="8" /> <TextView android:id="@+id/count" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:gravity="right" android:layout_marginRight="8dp"/> </LinearLayout>
サンプルコード
前回と同じリポジトリです。API実行時のカテゴリを適当に変えると別カテゴリのホッテントリ一覧が取得できます。