techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

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クラスを定義してみたら以下のような感じになった。

はてぶホッテントリRSSのPOJO Classes

要素名にコロンが入る要素は、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表示を実装してやれば以下のような簡易ホッテントリリーダの完成。

f:id:snishimura0926:20160417192552p:plain

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実行時のカテゴリを適当に変えると別カテゴリのホッテントリ一覧が取得できます。

*1:そらそうだ

*2:というより歴史的に考えると多分こっちが元祖という扱いになるのかな?

*3:そもそもなんて検索すればいいかがわからぬ