炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Kotlin + Retrofit2 + Gson で API 通信を実装する


アクトインディ Advent Calendar 2016 18日目の記事になります。子どもとおでかけ情報アプリ「いこーよ」は、マップ上で簡単におでかけ先を探せる検索アプリです。記事を見てくれたパパさん、ママさん、ぜひ一度使ってみてください^^

iko-yo.net

Kotlin 1.0.5
minSdkVersion 19
targetSdkVersion 25

この記事では Retrofit を使って API 通信をする際の、 http ヘッダーの追加方法や json を独自のモデルへ変更する方法などの基本的なところを書きたいと思います。iOS の開発を担当していた頃は Swift + Alamofire + ObjectMapper で API 通信を実装していたのですが、 Retrofit もとても分かりやすく、導入が容易で素晴らしいライブラリだと思いました。

  1. Retrofit のリクエストを生成する generater クラスを作成する
  2. Debug ビルド時には request と response のログを出力する
  3. api の json の形式に合わせて Gson の NamingPolicy を設定する
  4. http ヘッダー を追加する
  5. 401 エラー時の refreshToken を利用した token の更新方法

1. Retrofit のリクエストを生成する generater クラスを作成する

Retrofit では API のリクエスト先のエンドポイントを Interface で定義します。

interface SampleService {
    @GET("users/{id}")
    fun getUser(@Path("id") id: Int): Call<User>
}

定義した Interface から API のリクエストを生成するクラスを作成しておくと、何かと便利なので、まずは generator クラスを作成します。

class RetrofitServiceGenerator {
    companion object {
        fun createService(): SampleService {
            val apiUrl = "https://api.sample.com/"
            val retrofit = Retrofit.Builder()
                    .baseUrl(apiUrl)
                    .build()
            return retrofit.create(SampleService::class.java)
        }
    }
}

利用する時は val service = RetrofitServiceGenerator.createService() で service を作成し service.listRepos(user = "user") とします。あとは非同期でリクエストを投げたい場合は enqueue 、同期の時は execute で API をコールすることができます。

2. Debug ビルド時には request と response のログを出力する

開発中は request と response のログが見れた方が便利なので、Debug ビルドの時はログを出力するように設定します。Retrofit はデフォルトで OkHttp Client を利用しているので OkHttp の interceptor を追加してあげる形になります。 build.gradle に compile 'com.squareup.okhttp3:logging-interceptor:3.3.1' の追加が必要です。

class RetrofitServiceGenerator {
    companion object {
        fun createService(): SampleService {
            val apiUrl = "https://api.github.com/"
            val client = builderHttpClient() // OkHttpClient に logging の設定を追加
            val retrofit = Retrofit.Builder()
                    .baseUrl(apiUrl)
                    .client(client) // Retrofit に client を設定
                    .build()
            return retrofit.create(SampleService::class.java)
        }
    }

    private fun builderHttpClient(): OkHttpClient {
        val client = OkHttpClient.Builder()
        if (BuildConfig.DEBUG) {
            val logging = HttpLoggingInterceptor()
            logging.level = HttpLoggingInterceptor.Level.BODY
            client.addInterceptor(logging)
        }

        return client.build()
    }
}

3. api の json の形式に合わせて Gson の NamingPolicy を設定する

Gson を利用して json で返却されるレスポンスを独自の型に変換したいと思います。Retrofit はデフォルトの設定が Gson になっており、何も指定指定なくても json レスポンスを SampleService Interface で定義した型に変換を行います。 SampleService の Call の User が 変換されるデータクラス (Entity) になります。json と User は以下だとします。

{
    id: 1,
    first_name: "tabeo",
    last_name: "gohan"
}
data class User(
    val id: Int,
    val firstName: String,
    val lastName: String
)

この時 id は json と User で一致するので問題ありませんが、 first_name, firstName は snakecase と lowerUpperCase の違いがある為、このようなケースの場合は Gson の NamingPoricy を設定してあげる必要があります。このようなパターンは良くある為、予め Gson が snakecase と lowerUpperCase を適合させる LOWER_CASE_WITH_UNDERSCORES といった指定が準備されています。

class RetrofitServiceGenerator {
    companion object {
        fun createService(): SampleService {
            val apiUrl = "https://api.github.com/"
            val client = builderHttpClient()
            val gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create() // NamingPoricy そ指定する
            val retrofit = Retrofit.Builder()
                    .baseUrl(apiUrl)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create(gson)) // Retrofit に gson を設定
                    .build()
            return retrofit.create(SampleService::class.java)
        }
    }

    ... 省略 ...

}

4. http ヘッダー を追加する

http ヘッダーに token 情報を追加して API を call する場合など、 http ヘッダーに情報を追加するケースがあると思います。 http ヘッダーを追加する場合も logging と同様に OkHttp の Interceptor を作成して追加を行います。

class RetrofitServiceGenerator {
    companion object {
        fun createService(): SampleService {

          ... 省略 ...

        }

        private fun builderHttpClient(): OkHttpClient {
            val client = OkHttpClient.Builder()
                    .addInterceptor(BearerAuthenticationInterceptor()) // token を追加する interceptor
            if (BuildConfig.DEBUG) {
                val logging = HttpLoggingInterceptor()
                logging.level = HttpLoggingInterceptor.Level.BODY
                client.addInterceptor(logging)
            }

            return client.build()
        }
    }
}
class BearerAuthenticationInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain?): Response? {
        chain ?: return null

        val accessToken = "token" // アプリ内で保持している token をセットする
        val request = chain.request().newBuilder()
                .header("Authorization", "Bearer ${accessToken}")
                .build()
        return chain.proceed(request)
    }
}

5. 401 エラー時の refreshToken を利用した token の更新方法

OkHttp はレスポンスのコードが 401 の場合に http ヘッダーを更新して再リクエストできる Authenticator という便利なクラスが用意されています。 Authenticator クラスを継承したサブクラスを作ることで 401 エラーが返却された際の処理を実装することができます。

class TokenAuthenticator : Authenticator {
    private var count = 1

    override fun authenticate(route: Route, response: Response): Request? {
        // 3 回リトライを行う。 return null で authenticate メソッドの loop から抜ける
        if (this.retryCount(response = response) > 3) {
            return null
        }

        // アプリ内で保持している refreshToken
        val refreshToken = "refreshToken"
        // refreshToken を利用して token を更新する
        val newToken = this.updateToken(refreshToken = refreshToken) ?: return null

        return response.request().newBuilder().header("www-Authorization", "Bearer ${newToken}")?.build()
    }

    private fun updateToken(refreshToken: String): String? {
        // Retrofit の execute (同期)メソッドで token 更新のリクエストを行う
        return newToken
    }

    private fun retryCount(response: Response): Int {
        response.priorResponse()?.let {
            count += 1
        }
        return count
    }
}

作成した TokenAuthenticator を RetrofitServiceGenerator に反映した完成版がこちらです。

class RetrofitServiceGenerator {
    companion object {
        fun createService(): SampleService {
            val client = builderHttpClient()
            val gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create()
            val apiUrl = "https://api.github.com/"
            val retrofit = Retrofit.Builder()
                    .baseUrl(apiUrl)
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .client(client)
                    .build()
            return retrofit.create(SampleService::class.java)
        }

        private fun builderHttpClient(): OkHttpClient {
            val client = OkHttpClient.Builder()
                    .addInterceptor(BearerAuthenticationInterceptor())
                    .authenticator(TokenAuthenticator())
            if (BuildConfig.DEBUG) {
                val logging = HttpLoggingInterceptor()
                logging.level = HttpLoggingInterceptor.Level.BODY
                client.addInterceptor(logging)
            }

            return client.build()
        }
    }
}