nowで作成したAPIをKotlinとRetrofitとRxJavaとJacksonでGET/POSTする

いつもAndroidでRetrofit+RxJavaを使ってAPI通信する処理を書いてるが、今回はKotlinで挑戦してみる。
ついでに、使ったことのないnowもJacksonも触ってみた。

作成するもの

  • nowを使ったnodejs製のAPIモック(express)
  • Androidアプリ(Kotlin/Retrofit/RxJava/Jackson)

環境

API

準備

APIモックをexpressを使ったnodejsで作成し、nowにデプロイしていく。
nowについてはこの記事を参考にした。
以下、作業ログ

// APIモックのプロジェクト準備
$ mkdir try-now && cd try-now
// package.json作成
$ npm init -y
// expressを追加
$ npm i -S express
// apiの処理を書くjsを作成
$ touch index.js

// nowのインストール
$ npm i -G now
// nowのログイン(メールアドレスを入力すると確認メールが届くのでverifyする)
$ now login

実装

index.jsに以下を貼り付ける。
レスポンス変数は適当に作った。

var express = require('express')
var app = express()

app.get('/', function (req, res) {
  const json = {
    status: 1,
    card_status: 10,
    card_type: 'normal',
    card_id: "ca1111",
  }
  res.json(json)
})

app.post('/', function (req, res) {
  const json = {
    status: 2,
    card_status: 20,
    result_code: 1
  }
  res.json(json)
})

app.listen(3000)

ポートは3000を指定してるけど、なくても良い。

これでAPIモックはできあがった。

$ node index.js

としてローカルでも使えるが、今回はnowにデプロイする

デプロイ

nowでデプロイ、超簡単

$ now

https://try-now-kplvmhijbn.now.shというアドレスが表示されたので、これをAPIモックとして利用できる。

このアドレスに対してgetやpostをしていく。

Androidアプリ

使用するライブラリ

app/build.gradleに以下のライブラリ達を追加する

    // Retrofit2
    implementation 'com.squareup.retrofit2:retrofit:2.3.0'
    implementation 'com.squareup.retrofit2:converter-jackson:2.3.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'

    // RxJava2
    implementation "io.reactivex.rxjava2:rxjava:2.1.9"

    // RxAndroid
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

    // Jackson
    implementation 'com.fasterxml.jackson.core:jackson-core:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.4'
    implementation 'com.fasterxml.jackson.core:jackson-annotations:2.9.4'
    implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.4"

ソース:https://github.com/banbara23/try-now

実装

Retrofitのインタフェースを作成。

戻り値はRxjavaのObservableにする。

interface ApiService {
    @POST("/")
    fun post(): Observable<PostResponse>

    @GET("/")
    fun get(): Observable<GetResponse>
}

APIクライアントを作成していく。

今回、baseUrlにはnowで作成したurlを設定する。

class ApiClient {
    var apiService: ApiService

    init {
        val retrofit: Retrofit = Retrofit.Builder()
                .baseUrl("https://try-now-kplvmhijbn.now.sh")
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(JacksonConverterFactory.create(ObjectMapper()))
                .build()
        apiService = retrofit.create(ApiService::class.java)
    }

    /**
     * GETリクエスト
     */
    fun get(): Observable<GetResponse> =
            apiService.get()

    /**
     * POSTリクエスト
     */
    fun post(): Observable<PostResponse> =
            apiService.post()
}

APIクライアント呼び出しは以下

ラムダ初めて触った、すごい

  // get
        apiClient.post()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(
                        { res -> Log.d(TAG, res.toString()) },
                        { error -> Log.e(TAG, "{$error.message}") },
                        { Log.d(TAG, "post completed") }
                )

  // post
        apiClient.post()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(
                        { res -> Log.d(TAG, res.toString()) },
                        { error -> Log.e(TAG, "{$error.message}") },
                        { Log.d(TAG, "post completed") }
                )

モデルについては、 Response(共通レスポンス)を継承した各モデルが存在するイメージで作成した。

/**
 * 共通レスポンス
 */
open class Response(

        @field:JsonProperty("status") val status: Int = 0,
        @field:JsonProperty("card_status") val cardStatus: Int = 0
)
/**
 * GETレスポンスのモデル
 */
data class GetResponse(
        @field:JsonProperty("card_type") val cardType: String = "",
        @field:JsonProperty("card_id") val cardId: String = ""
) : Response() {
    override fun toString(): String =
            "GetResponse(status='$status' cardStatus='$cardStatus' cardType='$cardType', cardId='$cardId')"
}
/**
 * POSTレスポンスのモデル
 */
data class PostResponse(
        @field:JsonProperty("card_type") val cardType: String = "",
        @field:JsonProperty("card_id") val cardId: String = "",
        @field:JsonProperty("result_code") val resultCode: Int = 0
) : Response() {
    override fun toString(): String =
            "PostResponse(status='$status' cardStatus='$cardStatus' resultCode='$resultCode')"
}

この継承してるあたり、もうちょいうまくできないだろうか。

ビルドして実際に動作させると、Logcatに以下のログが表示されていればGET/POSTが成功している

getのログ
D/AppCompatActivity: GetResponse(status='1' cardStatus='10' cardType='normal', cardId='ca1111')
D/AppCompatActivity: get completed

postのログ
D/AppCompatActivity: PostResponse(status='2' cardStatus='20' resultCode='1')
D/AppCompatActivity: post completed

ソース:https://github.com/banbara23/Kotlin-Jackson-Sample

ハマったところ

Jacksonが@JsonPropertyを認識してくれない

APIレスポンスをJacksonでシリアライズする際、共通レスポンスモデルであるResponse.ktがKotlin用のアノテーションでないとエラーになる。

Jacksonが継承元の変数を@JsonPropertyだと認識してくれなかった。
継承先は@JsonPropertyでも大丈夫なのに…

NG : @JsonProperty
OK : @field:JsonProperty

実際のコード

/**
 * 共通レスポンス
 */
open class Response(
        // NG
        //@JsonProperty("status") val status: Int = 0,
        //@JsonProperty("card_status") val cardStatus: Int = 0

        // OK
        @field:JsonProperty("status") val status: Int = 0,
        @field:JsonProperty("card_status") val cardStatus: Int = 0
)

同様の問題が起きてないか調べたところ、以下のリンクに辿り着き、kotlinアノテーションを使うことで解決できた。 - https://github.com/FasterXML/jackson-module-kotlin/issues/56#issuecomment-321932309 - https://github.com/FasterXML/jackson-module-kotlin/issues/98#issuecomment-352016501

感想

nowが楽すぎてすごい

kotlinだとJavaよりコード量少なくて楽で良い