😺

retrofitとxmlとXmlPullParser

2023/02/11に公開

はじめに

retrofit で xml をパースする方法です。
XmlPullParser自体の使い方もあります。

環境

以下のライブラリを使用します。

  • com.squareup.retrofit2
  • org.xmlpull.v1.XmlPullParser

記事の流れ

まずはおそらく全員が共通で作成する必要がある以下を説明します。

  1. Converter クラスを作る(外側だけ)
  2. (1)の Factory クラスを作る
  3. Retrofit インスタンスに(2)を登録する

次に実際にxmlを解析するクラスを作ります。ここはどんなxmlを解析するかによって実装が異なります。

1. Converter クラスを作る

  • retrfit.Converterを実装します。generics は一つ目がokhttp3.ResponseBody、二つ目が開発者が結果として受け取りたい独自のデータクラスになります。
  • YourConverterは実際にxmlを解析する処理を含みます。(後述)
class YourResponseConverter: Converter<ResponseBody, YourModel> {

    private val parser = YourConverter()

    override fun convert(value: ResponseBody): YourModel {
        val bs = value.string().byteInputStream()
        return parser.parse(bs)
    }
}

2. Factory クラスを作る

class YourConverterFactory private constructor(): Converter.Factory() {
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *> {
        return YourResponseConverter()
    }

    companion object {
        fun create() = YourConverterFactory()
    }
}

3. Retrofit インスタンスに登録する

インスタンスの生成はhiltを想定して記述していますが、何でも大丈夫です。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Singleton
    @Provides
    fun provideXmlRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("YOUR-URL")
            .addConverterFactory(YourConverterFactory.create())
            .build()
    }
}

XMLPullParserでXMLを解析する。

parserの準備

以下のようにインスタンス化します。inputbyteStreamになります。

val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, null)

parserを次のタグへ移動する

nextTag()メソッドでparserを次のタグに移動します。
この時以下のようにrequireメソッドを使用してあげると、rssというスタートタグにいることを保障してくれます。

parser.nextTag()
parser.require(XmlPullParser.START_TAG, null, "rss")

以下のような場合、「今ココ」から「次ココ」に移動します。

<!-- 今ココ -->
<rss> <!-- 次ココ -->
<channel>
xxx
</channel>
</rss>

並列のタグを全て巡回する

以下のようなタグがあるとします。

並列タグのxmlの例
<title>xxx</title> <!-- 今ここ -->
<description>xxxのフィードです。</description>
<link>https://zenn.dev/xxx</link>
<image>
    <url>xxx</url>
    <title>xxxさんのフィード</title>
    <link>https://zenn.dev/xxx</link>
</image>
<language><![CDATA[ ja ]]></language>
<item>
    <title>xxx1</title>
    <description>xxx1</description>
    <link>https://zenn.dev/xxx1</link>
</item>
<item>
    <title>xxx2</title>
    <description>xxx2</description>
    <link>https://zenn.dev/xxx2</link>
</item>

next関数は次の要素へ移動します。(※タグではなく、次の要素です。)
parserの位置には種類があり、よく使用するのは、既に何度か出ているタグの開始を表すSTART_TAG、タグの終了を表すEND_TAG、テキストを表すTEXTなどです。
END_TAGになるまでループすることで、タグを全て巡回できます。またタグの開始は通常は常にSTART_TAGなので、それ以外はcontinueします。

なお各タグを読み込んだ際はnextTag()メソッドを呼んでおき、必ず1ループがSTART_TAGで終わるようにしておきましょう。(次で説明)

while (parser.next() != XmlPullParser.END_TAG) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        continue
    }
    // 取得したタグ名によって、処理を分岐する。
    // 不要なタグは、後述する方法でskipする。
    when (parser.name) {
        "title" -> readTitle(parser)
        "item" -> readItem(parser) 
        else -> skip()
    }
}

タグ内の要素を読み取る

以下のようなxmlがあり、titleタグの開始位置にいるとします。

<title> <!-- 今ココ -->
    xxxさんのフィード
</title>

開始タグにいる状態で、next()を呼ぶと次の要素に移動します。これはTextタイプなので、textプロパティを呼ぶことで、中身を取得できます。
中身を読み込んだ後はこのタグは用済みなので、nextTag()メソッドを呼ぶことで、次の開始タグに移動しておきます。

fun readTitle(parser: XmlPullParser): String {
    parser.require(XmlPullParser.START_TAG, null, "title")
    var result = ""
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.text
        parser.nextTag()
    }
    parser.require(XmlPullParser.END_TAG, null, tag)
    return result
}

不要なタグをスキップする

タグの巡回中に不要なタグがある場合、スキップしなければならないです。START_TAGがあったらdepthを+1、END_TAGがあったらdepthを-1とし、depthが0になったらそのタグは終了のため実質スキップしたことになります。
なお単純にnextTag()を呼ぶだけではだめなのか?と思うかもしれませんが、タグがネストされている場合はnextTag()ではネストに入り込んで次のタグに移動するので、だめです。

fun skip(parser: XmlPullParser) {
    if (parser.eventType != XmlPullParser.START_TAG) {
        throw ParseException("")
    }
    var depth = 1
    while (depth != 0) {
        when (parser.next()) {
            XmlPullParser.END_TAG -> depth--
            XmlPullParser.START_TAG -> depth++
        }
    }
}

終わりに

これでFeed系のアプリを作れるようになりました。😁
みなさんはどんなサービスで色々なサイトの最新情報を取得しているのでしょう?slackとか?lineとか?
それとも自作のFeedアプリですか・・・?

参考

GitHubで編集を提案

Discussion