最新の Retrofit の使い方についてまとめた（2022 年 10 月時点）

本記事は[Android Advent Calendar 2020 - Qiita](https://qiita.com/advent-calendar/2020/android)の 7 日目の記事です。

最近、今更ながら Retrofit で HTTP クライアントを実装しました。
使い方をネットで調べていたのですが、古い情報が多いように感じたので、これから Retrofit を導入したい人向けに最新の使い方をまとめます。

※2022 年 10 月に更新しました。

# Retrofit とは

[Retrofit](https://square.github.io/retrofit/)とは、型安全な Android 向けの HTTP クライアントライブラリです。
正確には[OkHttp](https://square.github.io/okhttp/)のラッパーで、アノテーションなどを使ってより実装しやすくするためのライブラリです。
[アプリ アーキテクチャ ガイド](https://developer.android.com/jetpack/guide?hl=ja)にも取り上げられており、HTTP クライアントを実装する上でメジャーなライブラリです。

# 導入方法

`app/build.gradle`に以下を追加してください。

```gradle:app/build.gradle
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
```

`com.squareup.retrofit2:converter-moshi`は JSON ライブラリの[Moshi](https://github.com/square/moshi)を Retrofit 内部で使うために必要です。
もしこのライブラリを使う場合、ライブラリ内部に Retrofit が含まれているので、以下の記述は不要になります。

```gradle:app/build.gradle
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
```

Moshi とは、同じ JSON ライブラリの[Gson](https://github.com/google/gson)の不満点を解消したライブラリです。
Kotlin との相性が良いため、特に理由がなければ Moshi をおすすめします。
詳細は以下の記事を参照してください。
[Android Retrofit2 と Moshi - Qiita](https://qiita.com/yoppie_x/items/bbc0ca7a4c8a4b29e141)

## Digest 認証をしたい場合

Retrofit もとい OkHttp で Digest 認証をする場合、`app/build.gradle`に追加で以下のライブラリを追加してください。

```gradle:app/build.gradle
implementation 'com.burgstaller:okhttp-digest:2.5'
```

# 使い方

例で載せているソースコードやデータはこちらで適当に用意したサンプルのため、そのままコピー&ペーストしても使えません。適宜読み替えてください。
また、API のデータ形式は JSON を扱う前提です。

## JSON 形式のレスポンスの取得

例えば以下の JSON を返却する API があるとします。

```json
{
  "users": [
    {
      "id": 1,
      "detail": "ユーザ1"
    },
    {
      "id": 2,
      "detail": "ユーザ2"
    },
    {
      "id": 3,
      "detail": "ユーザ3"
    },
    {
      "id": 4,
      "detail": "ユーザ4"
    }
  ]
}
```

メソッドは`GET`、エンドポイントは`api/v1/users/{user_name}/info`とします。

上記の例の場合、クライアント側の実装は以下のようになります。

```kotlin:UserService.kt
interface UserService {
    @GET("api/v1/users/{user_name}/info")
    suspend fun getUserInfo(
        @Path("user_name") userName: String,
        @Query("query") query: String
    ): Response<UserResponse>
}

data class UserResponse(
    // 変数名とJSONのキー名を一致させている
    val users: List<User>
)

data class User(
    // 変数名とJSONのキー名を一致させている
    val id: Int,
    val detail: String
)
```

`@GET("api/v1/users/{user_name}/info")`で、メソッドとエンドポイントを指定します。

メソッドは`@GET`以外にも`@POST`や`@PATCH`など用意されており、すべての API に対応できるはずです。

エンドポイントの中でパスが動的に変化する箇所（例で言うと`user_name`）がある場合、`@Path("user_name")`と指定することで、引数で渡されてきた値を設定できます。
GET なので URL パラメータを付与できます。付与する場合、`@Query("query")`と指定することで、引数で渡されてきた値を設定できます。
例えば`user_name`が`hoge`、`query`が`test`の場合、エンドポイントは`api/v1/users/hoge/info?query=test`になります。

コルーチンにも対応しているので、`suspend`関数として定義できます。

`Response`のジェネリクスに、JSON をでシリアライズする際のデータ型`UserResponse`を指定します。

注意点として、変数名と JSON のキー名を一致させる必要があります。
また、`@Json`アノテーションを使用してプロパティにキー名を指定する方法もあります。

```kotlin:User.kt
data class User(
    @Json(name = "id")
    val id: Int,
    @Json(name = "detail")
    val detail: String,
    @Json(name = "birth_day")
    val birthDay: String
)
```

その場合、`app/build.gradle`に追加で以下のライブラリを追加してください。

```gradle:app/build.gradle
implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
```

## JSON 形式のリクエストを送信する

例えば以下の JSON を送信してユーザ登録する API があるとします。

```json
{
    "id": 1,
    "detail": "ユーザ1",
    "birth_day": "20000101"
}
```

メソッドは`POST`、エンドポイントは`api/v1/users/register`とします。

上記の例の場合、クライアント側の実装は以下のようになります。

```kotlin:UserService.kt
interface UserService {
    @POST("api/v1/users/register")
    suspend fun registerUser(
        @Body user: User
    ): Response<Void>
}

data class User(
    @Json(name = "id")
    val id: Int,
    @Json(name = "detail")
    val detail: String,
    @Json(name = "birth_day")
    val birthDay: String
)
```

## Service の実体を生成

`UserService`はインタフェースなので、`Retrofit.Builder`で以下のようにインタフェースの実体を生成します。

```kotlin
// Timberを使う場合
val logging = HttpLoggingInterceptor {
    Timber.tag("OkHttp").d(it)
}
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)

val client = OkHttpClient.Builder()
    .addInterceptor(logging)
    .build()

val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

val userService = Retrofit.Builder()
    .baseUrl("https://qiita.com")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()
    .create(UserService::class.java)
```

もしロガーに[Timber](https://github.com/JakeWharton/timber)を使用している場合、`HttpLoggingInterceptor`の中で Timber を設定できます。
`addConverterFactory`で、どの JSON ライブラリを使うか指定できます。指定できる JSON ライブラリは以下の通りです。

- Gson
- Jackson
- Moshi

Moshi を使う場合は、`KotlinJsonAdapterFactory`を`Moshi.Builder`に add し、それを`MoshiConverterFactorycreate`の引数に設定します。

最後の`create`で生成したい Service のクラスを指定します。

また、上述した Digest 認証をする場合は以下のようになります。

```kotlin
// Digest認証に使用するUserNameとPasswordを設定する
val digestAuthenticator = DigestAuthenticator(Credentials("UserName", "Password"))
val authCache: Map<String, CachingAuthenticator> = ConcurrentHashMap()

// Timberを使う場合
val logging = HttpLoggingInterceptor {
    Timber.tag("OkHttp").d(it)
}
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)

val client = OkHttpClient.Builder()
    .authenticator(CachingAuthenticatorDecorator(digestAuthenticator, authCache))
    .addInterceptor(AuthenticationCacheInterceptor(authCache))
    .addInterceptor(logging)
    .build()

val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

val userService = Retrofit.Builder()
    .baseUrl("https://qiita.com")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()
    .create(UserService::class.java)
```

## 呼び出し方

生成後は先ほど定義したメソッドを以下のように呼び出します。
呼び出す際、`getUserInfo`は suspend 関数なので、別の suspend 関数から呼び出すか、コルーチンビルダー内で呼び出さないとビルドエラーになります。

```kotlin
launch {
    val response = userService.getUserInfo("hoge", "test")
    if (response.isSuccessful) {
        val users = response.body()
    }
}
```

`response.isSuccessful`はリクエストに成功した場合`true`となります。
`response.body()`は実際に取得した JSON をジェネリクスで指定したデータ型`UserResponse`として取得できます。

## HTTP ステータスしか返却しない場合

`Response`のジェネリクスに`Void`を指定します。
それ以外は同じなので説明は省略します。

```kotlin:UserVerificationService.kt
interface UserVerificationService {
    @GET("api/v1/user/verification")
    suspend fun getUserVerification(): Response<Void>
}
```

## ファイルのダウンロード

クライアント側の実装は以下のようになります。

```kotlin:DownloadService.kt
interface DownloadService {
    @GET
    suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
}
```

`@Url`で API のパス自体を動的に変更でき、任意の URL を渡せるようになります。
`Response`のジェネリクスに、バイナリデータを受け取れるよう`ResponseBody`を指定します。

呼び出し側の実装は以下のようになります。

```kotlin
launch {
    val fileUrl = "https://html5demos.com/assets/dizzy.mp4"
    val response = downloadService.downloadFile(fileUrl)
    if (response.isSuccessful) {
        val inputStream = response.body()?.byteStream() ?: return
        val file = File(context.getExternalFilesDir(null), "dizzy.mp4")
        DataInputStream(inputStream).use { dataInputStream ->
            FileOutputStream(file).use { fileOutputStream ->
                DataOutputStream(BufferedOutputStream(fileOutputStream)).use { dataOutStream ->
                    // データ読み込み
                    val b = ByteArray(4096)
                    var readByte: Int
                    while (-1 != dataInputStream.read(b).also { readByte = it }) {
                        dataOutStream.write(b, 0, readByte)
                    }
                    dataOutStream.flush()
                }
                fileOutputStream.flush()
            }
            dataInputStream.close()
        }
        inputStream.close()
    }
}
```

`response.body().byteStream()`でバイトデータを取得できるので、あとはファイルに書き込みます。
注意点として、最後に`inputStream.close()`を呼び出してリソースを解放する必要があります。

## ファイルのアップロード

クライアント側の実装は以下のようになります。

```kotlin:UploadService.kt
interface UploadService {
    @Multipart
    @POST("api/v1/upload")
    suspend fun postUpload(
        @Part file: MultipartBody.Part
    ): Response<Void>
}
```

`@Multipart`は`Content-Type: multipart/form-data`を送信するための定義です。
仕様によっては`@Multipart`がいらないそうです。詳しくは以下の記事を参照してください。

[Retrofit2 で MultipartPOST - Qiita](https://qiita.com/satsukies/items/822db4f68094dad4951d)

引数には`@Part`を指定して、フォームデータを送るための定義をします。
`multipart/form-data`については以下のサイトが参考になりました。

[【HTTP】multipart/form-data の boundary って何ぞや？ - Qiita](https://qiita.com/kunihiros/items/e11daacfd62d85d83c06)

呼び出し側の実装は以下のようになります。

```kotlin
launch {
    val file = File(context.getExternalFilesDir(null), "result.log")
    val requestBody = file.asRequestBody(MultipartBody.FORM)
    val multipartBody = MultipartBody.Builder("--*****")
        .addFormDataPart("file", file.name, requestBody)
        .build()
    uploadService.postUpload(multipartBody.part(0))
}
```

File クラスに拡張関数`asRequestBody`が追加されているので、これを利用してリクエストボディを生成します。その際、`MultipartBody.FORM`を指定して`Content-Type: multipart/form-data`を設定します。
最後に`MultipartBody.Builder`でマルチパートボディを生成します。
`Builder`の引数に設定している`--*****`は boundary です。
`addFormDataPart`で生成したリクエストボディを格納します。
`multipartBody.part`は List 型で、`MultipartBody.Builder`で格納した FormDataPart の数に応じて変化します。
今回は 1 つしか入れていないので、インデックス 0 を指定します。

## レスポンスからEnum型のパラメータに変換する

レスポンスから Enum 型のパラメータに変換したい場合、`@Json`アノテーションを使用して各列挙子にキー名を指定します。

[android - How use Kotlin enum with Retrofit? - Stack Overflow](https://stackoverflow.com/questions/45553339/how-use-kotlin-enum-with-retrofit)

```kotlin:UserType.kt
enum class UserType {
    @Json(name = "type_a")
    TYPE_A,
 
    @Json(name = "type_b")
    TYPE_B,
 
    @Json(name = "type_c")
    TYPE_C;
}
```

## DELETEメソッドにリクエストボティを付与する

Retrofit の仕様上、DELETE メソッドや GET メソッドで`@Body`を使った JSON リクエストはできないようです。

[RetrofitでDELETEメソッドに@Bodyつける - Qiita](https://qiita.com/TaigaNatto/items/7ea02949a0d8539ce7aa)

`@HTTP`アノテーションを利用することで解決できました。

```kotlin:UserService.kt
interface UserService {
    @HTTP(method = "DELETE", path = "api/v1/users/delete", hasBody = true)
    suspend fun deleteUser(@Body user: User): Response<Void>
}
```

## GETメソッドのクエリに配列を付与する

GET メソッドのクエリに配列を付与したい場合の実装は以下のようになります。

[【Android】RetrofitのGETリクエストで配列を渡したい - Qiita](https://qiita.com/circularuins/items/ed35a39bb25cc0c2fb16)

```kotlin:UserService.kt
interface UserService {
    @GET("api/v1/users")
    suspend fun getUsers(@Query("ids[]") vararg ids: Int): Response<List<User>>
}
```

## GETメソッドのクエリが複雑な場合

例えば下記サイトで挙げられているような、配列と連想配列の入れ子のようなケースの場合、`@Query`や`@QueryMap`では実現できないようでした。
また、解決策として`baseUrl()`を動的に変更することで Retrofit2 以前は対応できていたようですが、Retrofit2 からは対応できなくなりました。

[【Android】Retrofitの@GETで複雑なクエリを投げる - Qiita](https://qiita.com/i_greenwood/items/52604a0b1825b12d5b02)

Retrofit2 からは`@Url`アノテーションに API のクエリを含めた URL を渡すことで解決できました。

[android - Retrofit 2 - Dynamic URL - Stack Overflow](https://stackoverflow.com/questions/32559333/retrofit-2-dynamic-url)

```kotlin:UserService.kt
interface UserService {
    @GET
    suspend fun getUsers(@Url url: String): Response<List<User>>
}
```

# まとめ

最新の Retrofit の使い方についてまとめました。
古い記事の書き方では非推奨になっていることがあるので、このやり方を見て参考になれば幸いです。
もし間違っている点などあれば指摘いただけると嬉しいです。
何かあれば随時更新していきます。