retrofitとxmlとXmlPullParser
はじめに
retrofit で xml をパースする方法です。
XmlPullParser自体の使い方もあります。
環境
以下のライブラリを使用します。
com.squareup.retrofit2
org.xmlpull.v1.XmlPullParser
記事の流れ
まずはおそらく全員が共通で作成する必要がある以下を説明します。
- Converter クラスを作る(外側だけ)
- (1)の Factory クラスを作る
- 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の準備
以下のようにインスタンス化します。input
はbyteStream
になります。
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アプリですか・・・?
参考
-
XmlPullParserの公式ドキュメント
https://developer.android.com/training/basics/network-ops/xml?hl=ja#consume -
Retrofitの公式ドキュメント
https://square.github.io/retrofit/
Discussion