Retrofit × RxJava × Kotlin でイマドキのHttp通信をしよう（Android）

実務でKotlinを使ったAndroidアプリの開発にかかわれそうなので、
この機会に、イマドキの通信処理について勉強しました。

今回は、その過程で作ったサンプルアプリについて、ご紹介します。

## どういうアプリを作るのか

**http://weather.livedoor.com/forecast/**

上記URLのAPIに通信して、天気の情報をとってくるアプリを作ります。

取って来たデータをUIに反映する過程は省きますので、
「自分もこのアプリ作りたい」
と思った方は、UIを自作するか、
後ほど、このアプリのGitHubのURLを貼りますので、
そちらを参考にしてください。

## 必要なライブラリを取得する

アプリのbuild.gradleファイルに、必要な設定を追加します。

```groovy:build.gradle
dependencies {
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-kapt'

    ~~~~

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0"
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.retrofit2:converter-simplexml:2.3.0'
    implementation 'io.reactivex.rxjava2:rxkotlin:2.0.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
    implementation 'com.squareup.okhttp3:logging-interceptor:3.8.1'
}
```

## まずは定数クラスを作成
通信に使用する定数を、一つのクラスにまとめます。

```kotlin:HttpConstants.kt
/**
 * 通信で使用する定数を定義する
 */
class HttpConstants {

    companion object {

        // ========== 通信に関する定数群 =========== //
        /** 通信のタイムアウト秒数 */
        const val CONNECT_TIMEOUT_MS = 120L
        /** 読み取りのタイムアウト秒数 */
        const val READ_TIMEOUT_MS = 120L
        /** 最大リトライ回数 */
        const val RETRY_COUNT = 3L

        // ========== リクエストヘッダー =========== //
        const val HEADER_USER_AGENT = "User-Agent"

        // ========== URLのリスト =========== //
        /** ドメイン */
        const val URL_BASE = "http://weather.livedoor.com/forecast/"
        /** RSSの取得用API */
        const val END_POINT_RSS = "rss/primary_area.xml"
        /** 都市の天気情報取得API */
        const val END_POINT_CITY_WEATHER = "webservice/json/v1"
    }
}
```

User-Agentは、今回使用するAPIでは必要ありませんが、
共通ヘッダーの設定の仕方をお見せしたので、
仮で設定しています。


## ベースとなるHTTPクライアントを作成する

今回扱うデータには、JSONとXMLの２種類があるので、
それぞれについて、ベースとなるクライアントクラスを作ります。

作るベースクライアントは以下の３つです。

・ BaseClient
・ BaseJsonClient
・ BaseXmlClient

```kotlin:BaseClient.kt

import com.shunbou.retrofitsample.constant.HttpConstants
import com.shunbou.retrofitsample.util.DeviceUtils
import com.shunbou.retrofitsample.util.LogUtils
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import java.util.concurrent.TimeUnit

/**
 * 通信クラスの親クラス
 */
abstract class BaseClient {

    protected fun getHttpClient(): OkHttpClient {

        return OkHttpClient().newBuilder().apply {

            connectTimeout(HttpConstants.CONNECT_TIMEOUT_MS, TimeUnit.SECONDS)
            readTimeout(HttpConstants.READ_TIMEOUT_MS, TimeUnit.SECONDS)
            addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BODY
            })
            addInterceptor(HeaderInterceptor())
        }.build()
    }

    abstract fun getClient(): Retrofit

    /**
     * 非同期で通信する
     *
     * @param observable 通信ストリーム
     * @param onNext 通信成功後の処理
     * @param onError 通信失敗後の処理
     * @param onComplete 通信完了後の処理
     */
    fun <T> asyncRequest(
        observable: Observable<T>,
        onNext: ((T) -> Unit),
        onError: ((Throwable) -> Unit),
        onComplete: (() -> Unit)

    ): Disposable {

        return observable
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .retryWhen { observable ->
                observable.take(HttpConstants.RETRY_COUNT).flatMap {
                    return@flatMap Observable.timer(100, TimeUnit.MILLISECONDS)
                }

            }
            .subscribe({
                LogUtils.d(this::class.java.simpleName, "doOnNext : ${it.toString()}")
                onNext(it)
            }, {
                LogUtils.e(this::class.java.simpleName, "doOnError : ${it.message}")
                onError(it)
            }, {
                LogUtils.d(this::class.java.simpleName, "doOnComplete")
                onComplete()
            })

    }

    /**
     * 非同期で通信する
     *
     * @param single 通信ストリーム
     * @param onSuccess 通信成功後の処理
     * @param onError 通信失敗後の処理
     */
    fun <T> asyncSingleRequest(
        single: Single<T>,
        onSuccess: ((T) -> Unit),
        onError: ((Throwable) -> Unit)
    ): Disposable {

        return single
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .retry(HttpConstants.RETRY_COUNT)
            .subscribe({
                LogUtils.d(this::class.java.simpleName, "doOnSuccess : ${it.toString()}")
                onSuccess(it)
            }, {
                LogUtils.e(this::class.java.simpleName, "doOnError : ${it.message}")
                onError(it)
            })
    }

    class HeaderInterceptor : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            return chain.run {
                proceed(
                    request()
                        .newBuilder()
                        .addHeader(HttpConstants.HEADER_USER_AGENT, DeviceUtils.getModel())
                        .build()
                )
            }
        }
    }
}
```

**getHttpClinet()**で、
RetrofitでラップするHttpClientを作成しています。

addInterceptorで、共通ヘッダーを設定することが可能です。
ここでは、User-Agentを設定しています。

続いて、Json用と、XML用のクライアントを作成します。

```kotlin:BaseJsonClient.kt

import com.google.gson.Gson
import com.shunbou.retrofitsample.http.RxCallAdapterWrapperFactory
import com.shunbou.retrofitsample.util.UrlUtils
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

abstract class BaseJsonClient : BaseClient() {

    /**
     * 共通のクライアントを取得する
     */
    override fun getClient(): Retrofit = Retrofit.Builder().apply {

        client(getHttpClient())
        baseUrl(UrlUtils.getDomain())
        addCallAdapterFactory(RxCallAdapterWrapperFactory.create())
        addConverterFactory(GsonConverterFactory.create(Gson()))
    }.build()
}

```

RxCallAdapterWrapperFactoryについては、
後ほど説明します。

```kotlin:BaseXmlClient.kt

import com.shunbou.retrofitsample.http.RxCallAdapterWrapperFactory
import com.shunbou.retrofitsample.util.UrlUtils
import retrofit2.Retrofit
import retrofit2.converter.simplexml.SimpleXmlConverterFactory

abstract class BaseXmlClient : BaseClient() {

    /**
     * 共通のクライアントを取得する
     */
    override fun getClient(): Retrofit = Retrofit.Builder().apply {

        client(getHttpClient())
        baseUrl(UrlUtils.getDomain())
        addCallAdapterFactory(RxCallAdapterWrapperFactory.create())
        addConverterFactory(SimpleXmlConverterFactory.create())
    }.build()
}

```

BaseJsonClientとの違いは、ConverterFactoryだけです。


## CallAdapterを自作する

通信中に何かしらエラーが発生した場合、
onErrorに処理が入って来ます。

発生したエラーの種類に応じて処理分けしたいときなどは、
CallAdapterを自作して、onErrorが呼ばれる前に、
Throwableをラップしてしまうと便利です。

CallAdapterを自作するにあたって、
こちらを参考にしました。

>How to make RxErrorHandlingCallAdapterFactory?
https://stackoverflow.com/questions/43225556/how-to-make-rxerrorhandlingcalladapterfactory/43252881


```kotlin:RxCallAdapterWrapperFactory.kt

import com.shunbou.retrofitsample.http.exception.RetrofitException.Companion.asRetrofitException
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import java.lang.reflect.Type
import io.reactivex.functions.Function

class RxCallAdapterWrapperFactory private constructor() : CallAdapter.Factory() {

    companion object {

        fun create(): CallAdapter.Factory {
            return RxCallAdapterWrapperFactory()
        }
    }

    private val original = RxJava2CallAdapterFactory.create()

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        return RxCallAdapterWrapper(original.get(returnType, annotations, retrofit) ?: return null)
    }

    inner class RxCallAdapterWrapper<R>(private val wrapped: CallAdapter<R, *>) :
        CallAdapter<R, Any> {

        override fun responseType(): Type {
            return wrapped.responseType()
        }

        override fun adapt(call: Call<R>): Any {
            return when (val result = wrapped.adapt(call)) {
                is Single<*> -> result.onErrorResumeNext(Function { throwable ->
                    Single.error(
                        asRetrofitException(throwable)
                    )
                })
                is Observable<*> -> result.onErrorResumeNext(Function { throwable ->
                    Observable.error(
                        asRetrofitException(throwable)
                    )
                })
                is Completable -> result.onErrorResumeNext(Function { throwable ->
                    Completable.error(
                        asRetrofitException(throwable)
                    )
                })
                else -> result
            }
        }
    }
}
```

onErrorResumeNextを使うと、
Errorの発生をキャッチして、任意のStreamを返すことができます。

この場合では、
throwableを**RetrofitException**という自作したクラスでラップして、
onErrorに流しています。

```kotlin:RetrofitException.kt

import retrofit2.HttpException
import retrofit2.Response
import java.io.IOException

class RetrofitException private constructor(

    /** エラーメッセージ */
    var mMessage: String?,
    /** 接続先のURL */
    val mUrl: String?,
    /** Httpのレスポンス */
    val mResponse: Response<*>?,
    /** エラーの種別 */
    val mErrorType: ErrorType,
    val mException: Throwable

) : RuntimeException(mMessage, mException) {

    override fun toString(): String {
        return "${super.toString()} : $mErrorType : $mUrl : ${mResponse?.errorBody()?.string()}"
    }

    enum class ErrorType {

        /** サーバー通信中に発生したエラー */
        NETWORK,
        /** サーバーから200番台以外の、ステータスコードを受け取った場合のエラー */
        HTTP,
        /** それ以外のエラー */
        UNEXPECTED
    }

    companion object {

        /**
         * ステータスコードに関するエラーを生成する
         */
        private fun httpError(
            url: String,
            response: Response<*>,
            httpException: HttpException
        ): RetrofitException {
            val message = "${response.code()} ${response.message()}"
            return RetrofitException(message, url, response, ErrorType.HTTP, httpException)
        }

        /**
         * サーバー通信に関するエラーを生成する
         */
        private fun networkError(exception: IOException): RetrofitException {
            return RetrofitException(exception.message, null, null, ErrorType.NETWORK, exception)
        }

        /**
         * それ以外のエラーを生成する
         */
        private fun unexpectedError(exception: Throwable): RetrofitException {
            return RetrofitException(exception.message, null, null, ErrorType.UNEXPECTED, exception)
        }

        /**
         * RetrofitExceptionに返還する
         *
         * @param throwable 例外
         */
        fun asRetrofitException(throwable: Throwable): RetrofitException =
            when (throwable) {
                is RetrofitException -> throwable
                is HttpException -> {
                    val response = throwable.response()
                    httpError(response.raw().request().url().toString(), response, throwable)
                }
                is IOException -> networkError(throwable)
                else -> unexpectedError(throwable)
            }
    }
}
```

## 実際にAPIと通信するクライアントを作成

ベースクライアントができあがったので、
実際にAPIと通信するクライアントを作ります。

作るのは以下の２つです。
・ 都市IDの一覧を取得するクライアント（xml）
・ 指定した都市IDの天気の情報を取得するクライアント(json)

```kotlin:WeatherRssClient.kt

import com.shunbou.retrofitsample.constant.HttpConstants
import com.shunbou.retrofitsample.data.Rss
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import retrofit2.http.GET

/**
 * 全国地点定義表を取得する通信クライアント
 */
class WeatherRssClient : BaseXmlClient() {

    /**
     * 全国地点定義表を取得する
     *
     * @param onSuccess 通信成功後の処理
     * @param onError 通信失敗後の処理
     */
    fun getRss(
        onSuccess: ((Rss) -> Unit),
        onError: ((Throwable) -> Unit)
    ): Disposable {
        val single = getClient()
            .create(WeatherRssService::class.java)
            .getRss()

        return asyncSingleRequest(single, onSuccess, onError)
    }
}

/**
 * 全国地点定義表を取得するAPI
 */
interface WeatherRssService {

    @GET(HttpConstants.END_POINT_RSS)
    fun getRss(): Single<Rss>
}
```

WeatherRssClientで使用するデータクラスです。

```kotlin:Rss.kt

import android.text.TextUtils
import com.shunbou.retrofitsample.util.Diffable
import org.simpleframework.xml.*


@Root(name = "rss", strict = false)
class Rss {

    @set:Element(name = "channel")
    @get:Element(name = "channel")
    var channel: Channel? = null
}

@Root(name = "channel", strict = false)
class Channel {

    @set:Element(name = "copyright")
    @get:Element(name = "copyright")
    var copyRight: String? = null

    @set:Element(name = "source")
    @get:Element(name = "source")
    @Namespace(reference = "http://weather.livedoor.com/%5C/ns/rss/2.0", prefix = "ldWeather")
    var weatherSource: WeatherSource? = null
}

@Root(name = "source", strict = false)
class WeatherSource {

    @set:ElementList(name = "pref", inline = true)
    @get:ElementList(name = "pref", inline = true)
    var prefectureList: List<Prefecture>? = null
}

/**
 * 県の情報を格納するデータクラス
 */
@Root(name = "pref", strict = false)
class Prefecture {

    @set:ElementList(entry = "city", inline = true)
    @get:ElementList(entry = "city", inline = true)
    var cityList: List<City>? = null
}

/**
 * 都市の情報を格納するデータクラス
 */
@Root(name = "city", strict = false)
class City : Diffable {

    @set:Attribute(name = "title")
    @get:Attribute(name = "title")
    var title: String? = null

    @set:Attribute(name = "id")
    @get:Attribute(name = "id")
    var id: Int = 0

    @set:Attribute(name = "source")
    @get:Attribute(name = "source")
    var source: String? = null

    override fun isTheSame(other: Diffable): Boolean {
        return id == (other as? City)?.id
    }

    override fun isContentsTheSame(other: Diffable): Boolean {
        return TextUtils.equals(title, (other as? City)?.title)
    }
}
```

```kotlin:CityWeatherClient.kt

/**
 * 都市の天気情報を取得する通信クライアント
 */
class CityWeatherClient : BaseJsonClient() {


    /**
     * 都市の天気情報を取得する
     *
     * @param cityId 都市ID
     * @param onSuccess 通信成功後の処理
     * @param onError 通信失敗後の処理
     */
    fun getCityWeather(
        cityId: Int,
        onSuccess: ((CityWeather) -> Unit),
        onError: ((Throwable) -> Unit)
    ): Disposable {
        val single = getClient()
            .create(CityWeatherService::class.java)
            .getCityWeather(cityId)

        return asyncSingleRequest(single, onSuccess, onError)
    }
}

/**
 * 都市の天気情報を取得するAPI
 */
interface CityWeatherService {

    @GET(HttpConstants.END_POINT_CITY_WEATHER)
    fun getCityWeather(@Query("city") city: Int): Single<CityWeather>
}
```

CityWeatherClientで使用するデータクラスです。

```kotlin:CityWeather.kt

import com.shunbou.retrofitsample.util.Diffable

/**
 * 都市の天気情報を格納するデータクラス
 */
data class CityWeather(
    var publicTime: String? = null,
    var title: String? = null,
    var description: Description? = null,
    var forecasts: ArrayList<Forecast> = ArrayList()
)

data class Description(
    var text: String? = null
)

data class Forecast(
    var dateLabel: String? = null,
    var telop: String? = null,
    var date: String? = null,
    var temperature: Temperature? = null,
    var image: Image? = null
)

data class Temperature(
    var min: TemperatureDescription? = null,
    var max: TemperatureDescription? = null
)

data class TemperatureDescription(
    var celsius: Float = 0F,
    var fahrenheit: Float = 0F
)

data class Image(
    var title: String? = null,
    var url: String? = null
)
```

## 終わりに

サンプルアプリのコードは、Githubにあげていますので、
参考にしてください

https://github.com/shunm-999/retrofitSample