本記事はAndroid Advent Calendar 2020 - Qiitaの 7 日目の記事です。
最近、今更ながら Retrofit で HTTP クライアントを実装しました。
使い方をネットで調べていたのですが、古い情報が多いように感じたので、これから Retrofit を導入したい人向けに最新の使い方をまとめます。
※2022 年 10 月に更新しました。
Retrofit とは
Retrofitとは、型安全な Android 向けの HTTP クライアントライブラリです。
正確にはOkHttpのラッパーで、アノテーションなどを使ってより実装しやすくするためのライブラリです。
アプリ アーキテクチャ ガイドにも取り上げられており、HTTP クライアントを実装する上でメジャーなライブラリです。
導入方法
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 が含まれているので、以下の記述は不要になります。
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
Moshi とは、同じ JSON ライブラリのGsonの不満点を解消したライブラリです。
Kotlin との相性が良いため、特に理由がなければ Moshi をおすすめします。
詳細は以下の記事を参照してください。
Android Retrofit2 と Moshi - Qiita
Digest 認証をしたい場合
Retrofit もとい OkHttp で Digest 認証をする場合、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
とします。
上記の例の場合、クライアント側の実装は以下のようになります。
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
アノテーションを使用してプロパティにキー名を指定する方法もあります。
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
に追加で以下のライブラリを追加してください。
implementation "com.squareup.moshi:moshi-kotlin:1.13.0"
JSON 形式のリクエストを送信する
例えば以下の JSON を送信してユーザ登録する API があるとします。
{
"id": 1,
"detail": "ユーザ1",
"birth_day": "20000101"
}
メソッドはPOST
、エンドポイントはapi/v1/users/register
とします。
上記の例の場合、クライアント側の実装は以下のようになります。
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 を使う場合は、KotlinJsonAdapterFactory
をMoshi.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
を指定します。
それ以外は同じなので説明は省略します。
interface UserVerificationService {
@GET("api/v1/user/verification")
suspend fun getUserVerification(): Response<Void>
}
ファイルのダウンロード
クライアント側の実装は以下のようになります。
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()
を呼び出してリソースを解放する必要があります。
ファイルのアップロード
クライアント側の実装は以下のようになります。
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
引数には@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
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
アノテーションを利用することで解決できました。
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
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
interface UserService {
@GET
suspend fun getUsers(@Url url: String): Response<List<User>>
}
まとめ
最新の Retrofit の使い方についてまとめました。
古い記事の書き方では非推奨になっていることがあるので、このやり方を見て参考になれば幸いです。
もし間違っている点などあれば指摘いただけると嬉しいです。
何かあれば随時更新していきます。