search
LoginSignup
8

More than 3 years have passed since last update.

Organization

Kotlinで型安全にWeb APIを呼び出す

この記事はVALU Advent Calendarの19日目の記事です。

Kotlin、いいですよね。

良いところは色々あるんですが、data classはほんとに好きで、カジュアルにプリミティブ型をラップした型を作れるのがいいですよね。

たとえば、次のようなメソッドを見ると絶望しませんか?

someMethod.kt

fun getRepository(accessKey: String, username: String, repositoryName: String, branchName: String) : Single<RepositoryInfo>

ソースコードのホスティングサービスから、

  • アクセスキー
  • リポジトリの所有ユーザ名
  • リポジトリ名

これらの情報を渡して何らかの情報を受け取る、みたいなイメージです。

が。

引数が、これでもかというくらいStringですね。
何が辛いって、明らかに間違った呼び出し方をしていることを、コンパイラで検出できないことです。

caller.kt
// 正しい呼び方
getRepository("Lzizwn4vrw3k/bLVi/+16QcGIalE4Zts1bCjyBPK5M0=", "_pochi", "awesome_repository")

// 明らかに間違った呼び方 (引数の順番が違う)
getRepository("_pochi", "awesome_repository", "Lzizwn4vrw3k/bLVi/+16QcGIalE4Zts1bCjyBPK5M0=")

このように引数の順番を間違えたとしても、コンパイラは間違いを検出できません。
実行時に処理が失敗して、はじめて間違いに気づきます。
また、このメソッドの引数が次のように増えたとしましょう。

someMethod.kt
fun getRepository(accessKey: String, username: String, repositoryName: String, branchName: String) : Single<RepositoryInfo>

ブランチ名が増えました。こんなときに既存のコードを修正する時に、うっかり引数の順番を間違えてしまうこともあるでしょう。つらいですね。

そこで、各引数をdata classとして定義すると、以下のようになります。

someMethod.kt
data class AccessKey(val value: String)
data class UserName(val value: String)
data class RepositoryName(val value: String)
data class BranchName(val value: String)

fun getRepository(accessKey: AccessKey, username: UserName, repositoryName: RepositoryName, branchName: BranchName) : Single<RepositoryInfo>

こうすれば、引数の順番が間違っている場合には、コンパイル時に間違いに気づくことができます。

本題

本題です。Retrofit2でAPIクライアントを定義している場合に、data classをどう扱うか、という話です。

API.kt
interface ServiceApi {
    @GET("home/feeds")
    fun getRepositoryInfo(@Header("Authorization") accessToken: AccessToken, @Query("username") userName: UserName, @Query("repository") repository: RepositoryName, @Query("branch") branchName: BranchName): Single<Response>

上記のようにIFを定義しました。
ただしこれだと、サーバに正しくパラメータを渡すことが出来ません。
RetrofitはtoString()した値をサーバに渡してしまいます。

toString()をオーバーライドする・・・という手もなくはないのですが、あまりやりたくないですよね。

そこでRetrofitが用意しているConverterという仕組みを使います。

以下のようにConverter.Factoryを継承したクラスをつくり、stringConverterをオーバーライドします。ここで、どの型を、どのような文字列に変換するかを定義します。

ParameterConverter.kt
class ParameterConverter : Converter.Factory() {
    override fun stringConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<*, String>? =
            when (type) {
                // AccessTokenのときは、頭にBearerをつけてあげる
                AccessToken::class.java -> Converter<AccessToken, String> { it -> it.toBearerToken() }
                UserName::class.java -> Converter<UserName, String> { it -> it.value }
                RepositoryName::class.java -> Converter<RepositoryName, String> { it -> it.value }
                BranchName::class.java -> Converter<BranchName, String> { it -> it.value }
                // enumを適切な文字列に変換してあげたりもできる
                OrderType::class.java -> Converter<OrderType, String> {
                    when (it) {
                        NONE -> "none"
                        DESC -> "desc"
                        ASC -> "asc"
                        null -> "none"
                    }
                }
                else -> Converter<Any, String> { it -> it.toString() }
            }
}

Retrofitをbuildするときに、addConverterFactoryでコンバータを差し込みます。

provideRetrofit.kt
    fun provideRetrofit(okHttpClient: OkHttpClient, app: Application, moshi: Moshi): Retrofit {
        return Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl("https://hogehoge")
                // コンバータを差し込む
                .addConverterFactory(ParameterConverter())
                .build()
    }

これで、型安全にWebAPIを呼び出せるようになります!うれしい!!!

おわりに

今回私、VALU Advent CalendarでAndroid開発ネタをいくつか書いてきました。
こんなかんじで、iOSだけでなく、Androidアプリ開発も弊社頑張っております!!

「でも御社Androidアプリリリースしてないじゃん?」といわれるとまぁ、なんかあれなんですけど。

...まだ今年はちょうど2週間ありますが,現在も,来年以降も,どんどん VALU のアプリは進化していきますので,応援の程よろしくお願い致します!!!

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
What you can do with signing up
8