Kotlin + Retrofit2 + Gson で API 通信を実装する
アクトインディ Advent Calendar 2016 18日目の記事になります。子どもとおでかけ情報アプリ「いこーよ」は、マップ上で簡単におでかけ先を探せる検索アプリです。記事を見てくれたパパさん、ママさん、ぜひ一度使ってみてください^^
Kotlin 1.0.5 minSdkVersion 19 targetSdkVersion 25
この記事では Retrofit を使って API 通信をする際の、 http ヘッダーの追加方法や json を独自のモデルへ変更する方法などの基本的なところを書きたいと思います。iOS の開発を担当していた頃は Swift + Alamofire + ObjectMapper で API 通信を実装していたのですが、 Retrofit もとても分かりやすく、導入が容易で素晴らしいライブラリだと思いました。
- Retrofit のリクエストを生成する generater クラスを作成する
- Debug ビルド時には request と response のログを出力する
- api の json の形式に合わせて Gson の NamingPolicy を設定する
- http ヘッダー を追加する
- 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
{ 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() } } }