93
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AndroidAdvent Calendar 2020

Day 7

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

Last updated at Posted at 2020-12-06

本記事はAndroid Advent Calendar 2020 - Qiitaの 7 日目の記事です。

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

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

Retrofit とは

Retrofitとは、型安全な Android 向けの HTTP クライアントライブラリです。
正確にはOkHttpのラッパーで、アノテーションなどを使ってより実装しやすくするためのライブラリです。
アプリ アーキテクチャ ガイドにも取り上げられており、HTTP クライアントを実装する上でメジャーなライブラリです。

導入方法

app/build.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を Retrofit 内部で使うために必要です。
もしこのライブラリを使う場合、ライブラリ内部に Retrofit が含まれているので、以下の記述は不要になります。

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

Moshi とは、同じ JSON ライブラリのGsonの不満点を解消したライブラリです。
Kotlin との相性が良いため、特に理由がなければ Moshi をおすすめします。
詳細は以下の記事を参照してください。
Android Retrofit2 と Moshi - Qiita

Digest 認証をしたい場合

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

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

使い方

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

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

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

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

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

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

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_namehogequerytestの場合、エンドポイントはapi/v1/users/hoge/info?query=testになります。

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

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

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

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に追加で以下のライブラリを追加してください。

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

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

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

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

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

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

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で以下のようにインタフェースの実体を生成します。

// 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を使用している場合、HttpLoggingInterceptorの中で Timber を設定できます。
addConverterFactoryで、どの JSON ライブラリを使うか指定できます。指定できる JSON ライブラリは以下の通りです。

  • Gson
  • Jackson
  • Moshi

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

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

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

// 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 関数から呼び出すか、コルーチンビルダー内で呼び出さないとビルドエラーになります。

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

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

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

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

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

ファイルのダウンロード

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

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

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

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

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()を呼び出してリソースを解放する必要があります。

ファイルのアップロード

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

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

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

Retrofit2 で MultipartPOST - Qiita

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

【HTTP】multipart/form-data の boundary って何ぞや? - Qiita

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

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

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

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

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

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

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

android - Retrofit 2 - Dynamic URL - Stack Overflow

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

まとめ

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

93
85
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
93
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?