[Android]Retrofitのレスポンスのハンドリングを楽にするかもしれない2つのライブラリ

# はじめに

Android 開発において [Retrofit](https://github.com/square/retrofit) はよく使われるライブラリの一つかと思います。

今回は Retrofit のレスポンスのハンドリングを楽にしてくれるかもしれないライブラリを2つご紹介したいと思います。

- [slackhq/EitherNet](https://github.com/slackhq/EitherNet)
- [skydoves/Sandwich](https://github.com/skydoves/Sandwich)

# 2つのライブラリの特徴

この2つのライブラリの共通の特徴として、ライブラリが定義している独自のクラスでレスポンスを受け取れます。

`EitherNet` の方は `ApiResult`、`Sandwich` の方は `ApiResponse` という独自のクラスが定義されており、これらのクラスは sealed class になっています。

これらの sealed class のサブクラスが通信の成功や失敗を表す型になっているため、クラスの型を判別すれば通信処理の結果が判定できます。

# Retrofit のエラーハンドリングは手間がかかる？

例えば Qiita の API で記事一覧を取得することを考えます。

まずは以下のように Retrofit で使うためのインターフェースを定義します。

```kotlin
interface QiitaApi {
    @GET("/api/v2/items")
    suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): List<Item>
}
```

この場合、記事一覧の取得は以下のように呼び出します。

```kotlin
suspend fun fetchItems() {
    val items = qiitaApi.findItems(1, 30)
    // items を使った処理...
}
```

ただ、この場合通信処理を行うので、もしかしたらネットワーク状態が悪くて通信が失敗してしまうこともあり得ます。

その場合、以下のようにエラーハンドリングを行う必要があります。

```kotlin
suspend fun fetchItems() {
    try {
        val items = qiitaApi.findItems(1, 30)
        // items を使った処理...
    } catch (e: Throwable) {
        // エラー処理...
    }
}
```

また API からレスポンスは返ってきたけど、HTTP ステータスコードが400以上の場合にはレスポンスの内容をパースして別に処理を行いたい、などの場合も考えられます。

その場合は Retrofit の `HttpException` の例外を捕捉して処理する必要があります。

```kotlin
suspend fun fetchItems() {
    try {
        val items = qiitaApi.fetchItems(1, 30)
        // items を使った処理...
    } catch (e: HttpException) {
        if (e.code() == 400) {
            val errorBody = e.response()?.errorBody()
            // HTTP ステータスコードが400の場合のエラー処理...
        }
    } catch (e: Throwable) {
        // エラー処理...
    }
}
```

このように Retrofit で API 呼び出し時に自前でエラーハンドリングをするのは結構手間がかかってしまうかもしれません。

そこで先ほど挙げた2つのライブラリを使って、レスポンス時の処理を変更してみようと思います。

# slackhq/EitherNet

`EitherNet` の方では `ApiResult<out T, out E>` でレスポンスの結果を受け取ることができ、v0.2.0 時点での定義は以下のようになっています。

```kotlin
public sealed class ApiResult<out T, out E> {

  public data class Success<T : Any>(public val response: T) : ApiResult<T, Nothing>()

  public sealed class Failure<out E> : ApiResult<Nothing, E>() {

    // 通信エラー.
    public data class NetworkFailure internal constructor(public val error: IOException) : Failure<Nothing>()
    // 予期しないエラー（レスポンスの JSON のパースエラーなど）.
    public data class UnknownFailure internal constructor(public val error: Throwable) : Failure<Nothing>()
    // HTTP ステータスコードが 4xx or 5xx の時のエラー.
    public data class HttpFailure<out E> internal constructor(public val code: Int, public val error: E?) : Failure<E>()
    // レスポンスボディを自前で変換する時に ApiException を throw した際のエラー.
    public data class ApiFailure<out E> internal constructor(public val error: E?) : Failure<E>()

  }
}
```

`T` は成功時の型、`E` はエラー時の型になります。

`ApiResult` には2つのサブクラス `Success` と `Failure` があり、それぞれ通信の成功と失敗を表す型になっています。

`Failure` にはさらに4つのサブクラスが定義されていて、エラーの発生要因によってそれぞれ対応するクラスのインスタンスがレスポンスとして返るようになります。


## 使い方

例として、先ほどの Qiita の API に対して対応してみます。

まずは通信成功、失敗時のレスポンスのデータクラスを作ります。

レスポンスの JSON 文字列をパースするために [Moshi](https://github.com/square/moshi) を使います。

```kotlin
// Qiita の記事の投稿データ.
// https://qiita.com/api/v2/docs#投稿
@JsonClass(generateAdapter = true)
data class Item(
    @Json(name = "id") val id: String,
    @Json(name = "title") val title: String
)

// Qiita の API のエラーレスポンス
// https://qiita.com/api/v2/docs#エラーレスポンス
@JsonClass(generateAdapter = true)
data class QiitaApiError(
    @Json(name = "message") val message: String,
    @Json(name = "type") val type: String
)
```

Qiita API のエラーレスポンスは `message` と `type` というキーを含んだ JSON オブジェクトの文字列が返ってくるので、上記のように定義することができます。

```kotlin
interface QiitaApi {
    @DecodeErrorBody // HttpFailure 時にレスポンスボディを QiitaApiError に変換するために必要
    @GET("/api/v2/items")
    suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): ApiResult<List<Item>, QiitaApiError>
}

val qiitaApi = Retrofit.Builder()
    .addConverterFactory(ApiResultConverterFactory)
    .addCallAdapterFactory(ApiResultCallAdapterFactory)
    .addConverterFactory(MoshiConverterFactory.create()) // addConverterFactory(ApiResultConverterFactory) より後に設定する必要がある
    .build()
    .create<QiitaApi>()
```

Retrofit のインターフェースの定義と作成は上記のように行います。

`ApiResult<List<Item>, QiitaApiError>` は通信成功時には `List<Item>`、通信失敗（`HttpFailure` or `ApiFailure`）時には `QiitaApiError`　でレスポンスを受け取ることができます。

`@DecodeErrorBody` アノテーションを付けないとエラー時のレスポンスボディを `QiitaApiError` に変換してくれないので注意が必要です。

もう一つ注意点としては、Retrofit のインスタンスを作成する際の `addConverterFactory` の呼び出しの順序です。

`addConverterFactory(ApiResultConverterFactory)` より後に `addConverterFactory(MoshiConverterFactory.create())` を呼び出さないと正しくレスポンスがパースされないので注意してください。

そして以下のように呼び出して、結果をハンドリングすることができます。

```kotlin
when (val result = qiitaApi.fetchItems(1, 3)) {
    is ApiResult.Success -> Log.d(TAG, "Success: ${result.response}")
    is ApiResult.Failure -> when (result) {
        // result.error は QiitaApiError 型のインスタンス
        is ApiResult.Failure.HttpFailure -> Log.e(TAG, "HttpFailure: ${result.code}, ${result.error}")
        is ApiResult.Failure.ApiFailure -> Log.e(TAG, "ApiFailure: ${result.error}")
        is ApiResult.Failure.NetworkFailure -> Log.e(TAG, "NetworkFailure: ${result.error}")
        is ApiResult.Failure.UnknownFailure -> Log.e(TAG, "UnknownFailure: ${result.error}")
    }
}
```

# skydoves/Sandwich

`Sandwich` の方では `ApiResponse<out T>` で結果を受け取ることができ、v1.1.0 時点での定義は以下のようになっています。

```kotlin
sealed class ApiResponse<out T> {

  data class Success<T>(val response: Response<T>) : ApiResponse<T>() {
    val statusCode: StatusCode = getStatusCodeFromResponse(response)
    val headers: Headers = response.headers()
    val raw: okhttp3.Response = response.raw()
    val data: T? = response.body()
    override fun toString() = "[ApiResponse.Success](data=$data)"
  }

  sealed class Failure<T> {
    
    // HTTP ステータスコードが200番台以外のエラー（ただし設定で変更は可能）.
    data class Error<T>(val response: Response<T>) : ApiResponse<T>() {
      val statusCode: StatusCode = getStatusCodeFromResponse(response)
      val headers: Headers = response.headers()
      val raw: okhttp3.Response = response.raw()
      val errorBody: ResponseBody? = response.errorBody()
      override fun toString(): String = "[ApiResponse.Failure.Error-$statusCode](errorResponse=$response)"
    }

    // Failure.Error 以外のエラー.
    data class Exception<T>(val exception: Throwable) : ApiResponse<T>() {
      val message: String? = exception.localizedMessage
      override fun toString(): String = "[ApiResponse.Failure.Exception](message=$message)"
    }
  }
}
```

`T` は成功時の型になります。

`EitherNet` と同じように `ApiResponse` にも2つのサブクラス `Success` と `Failure` があり、それぞれ通信の成功と失敗を表す型になっています。

`Failure` にはさらに2つのサブクラス `Error` と `Exception` が定義されています。

`Failure.Error` は HTTP ステータスコードが200番台以外の場合のエラー（ただし設定で変更することが可能）で、それ以外のエラーの場合は `Failure.Exception` になります。


## 使い方

`Sandwich` の README を見るとわかりますが、このライブラリは多くの機能が備わっています。

今回は `EitherNet` と同じように suspend 関数で API 呼び出しをする際の使い方について、簡単に紹介します。

```kotlin
interface QiitaApi {
    @GET("/api/v2/items")
    suspend fun fetchItems(@Query("page") page: Int, @Query("per_page") perPage: Int): ApiResponse<List<Item>>
}

val qiitaApi = Retrofit.Builder()
    .addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())
    .addConverterFactory(MoshiConverterFactory.create())
    .build()
    .create<QiitaApi>()
```

Retrofit のインターフェースの定義と作成は上記のように行います。

`ApiResponse` 型の戻り値の suspend 関数を呼び出すには `addCallAdapterFactory(CoroutinesResponseCallAdapterFactory())` を設定する必要があります。

そして以下のように呼び出して、結果をハンドリングすることができます。

```kotlin
when (val result = qiitaApiForSandwich.fetchItems(1, 3)) {
    is ApiResponse.Success -> Log.e(TAG, "Success: ${result.response.body()}")
    is ApiResponse.Failure.Error -> Log.e(TAG, "Error: ${result.statusCode}, ${result.errorBody?.string()}")
    is ApiResponse.Failure.Exception -> Log.e(TAG, "Exception: ${result.exception}")
}
```

エラー時のレスポンスボディをパースして `QiitaApiResult` に変換したい場合などは、[Mapper](https://github.com/skydoves/Sandwich#mapper) などの機能を使って変換する必要があります。

`Failure.Error` 時に `Mapper` を適用して `QiitaApiResult?` に変換する例は以下になります。

```kotlin
class QiitaApiErrorMapper(private val moshi: Moshi) : ApiErrorModelMapper<QiitaApiError?> {
    override fun map(apiErrorResponse: ApiResponse.Failure.Error<*>): QiitaApiError? {
        return apiErrorResponse.errorBody?.source()?.let(moshi.adapter(QiitaApiError::class.java)::fromJson)
    }
}


when (val result = qiitaApiForSandwich.fetchItems(1, 3)) {
    is ApiResponse.Failure.Error -> result.onError(QiitaApiError.Mapper(moshi)) { Log.e(TAG, "Error: ${this?.message}") }
    ...
}
```

# 2つのライブラリの比較

私が感じた `EitherNet` と `Sandwich` との違いについてです。

## 使いやすさ

`EitherNet` の方がシンプルに思いました。

`Sandwich` は多機能なため、機能を把握するのに少し大変かもしれません。


## エラーハンドリングについて

`EitherNet` はエラー時のレスポンスの型も設定できるので、Moshi と併用すればエラーレスポンスの変換が簡単に行えました。

`Sandwich` の方は `Mapper` を作成してエラーレスポンスを変換する必要があるので、少しだけ手間が増える感じです。

## レスポンスヘッダへのアクセス

レスポンスヘッダの中身を扱いたい場合は `Sandwich` 一択になります。

`Sandwich` では `ApiResponse.Success` と `ApiResponse.Failure.Error` のインスタンスでレスポンスヘッダを扱うことができますが、`EitherNet` ではできません。

## メンテナンス状況

2021年6月現在、`EitherNet` は最近あまりメンテナンスされていないようです。

`Sandwich` は定期的に機能追加などのメンテナンスが行われているようです。


# 最後に

簡単にですが `EitherNet` と `Sandwich` について紹介させていただきました。

Retrofit のレスポンスのハンドリングが楽になるかもしれないので、興味のある方は使ってみてはいかがでしょうか。

https://github.com/watabee/RetrofitResponsesSample にサンプルプロジェクトを公開しています。