[Kotlin][Android] Retrofit + KotlinでChatwork APIを叩く

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。Steam厨のこむろ@札幌です。ここ最近はPAYDAY2です。 *1

お盆を過ぎたら札幌はめっきり朝夕冷え込むようになりました。本当にエアコンはいらない土地でした(驚愕

今回は、弊社諏訪ネ申のRxAndroid+Retrofitの記事 にインスパイアされて書きました。

はじめに

ChatworkのREST APIをコールするAndroidアプリケーションを作成します。今回は、GETPOST を実装してみます。

Retrofit + Kotlin

基本的にはJavaの時とあまり変わりませんが、大きく違うことがあります。 null を許容する値を大幅に駆逐できることです。

Retrofitを利用する際には、レスポンスJSONをマッピングするためのオブジェクトが必要になります。Kotlinでは以下のように記述することが出来ました。 破壊的代入を可能にする varnull を許容する ? も書かずにオブジェクトを定義することができました。

この記事では、 チャットルーム一覧の取得メッセージ投稿 のAPIをコールするアプリを作成してみます。

REST APIを実行するための準備

Retrofitを利用してREST APIの呼び出しを定義するための準備をしましょう。

RestAdapterの作成

処理を呼び出したい箇所でRetrofitのRestAdapterを作成します。この辺りの実装は こちら を参考にしています。JsonParserにはGsonを利用します。

// chatwork API endpoint
private val ENDPOINT = "https://api.chatwork.com/v1"

val gsonBuilder = GsonBuilder()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .registerTypeAdapter(javaClass<Date>(), DateTypeAdapter()).create()

val restAdapter = RestAdapter.Builder()
            .setEndpoint(ENDPOINT)
            .setConverter(GsonConverter(gsonBuilder))
            .setLogLevel(RestAdapter.LogLevel.FULL)
            .setLog(AndroidLog("=NETWORK="))
            .build();

特に認証など必要ないAPIであればこれだけで問題ありません。 今回は、Chatwork APIを対象にしているため、認証情報をヘッダに埋め込む必要があります。

API TOKENをヘッダに埋め込む

ヘッダ情報に情報を追加する場合は、Retrofitでは RequestInterceptor を利用します。

今回の場合はChatwork APIのAPI TOKENを埋め込むので以下のように記述します。

private val API_KEY = "<Your API Token>"
private val TOKEN_KEY = "X-ChatWorkToken"

val requestInterceptor: RequestInterceptor = object: RequestInterceptor {
        override fun intercept(request: RequestInterceptor.RequestFacade?) {
            request?.let {
                it.addHeader(TOKEN_KEY, API_KEY)
            }
        }
    }

RequestInterceptor にAPI TOKENを追加したら、先ほど定義したRestAdapterにセットします。

val restAdapter = RestAdapter.Builder()
            .setEndpoint(ENDPOINT)
            .setRequestInterceptor(requestInterceptor)
            .setConverter(GsonConverter(gsonBuilder))
            .setLogLevel(RestAdapter.LogLevel.FULL)
            .setLog(AndroidLog("=NETWORK="))
            .build();

これでChatwork APIをコールするための準備が整いました。

チャットルーム一覧を取得する

自分のチャット一覧取得 を確認してみるとAPIを GET で引数なしで呼び出す必要があるようです。1つずつ準備をしていきましょう。

レスポンスをマッピングするオブジェクトを定義する

ChatRoom一覧APIは以下の様なJSONがレスポンスとして返却されます。

[
  {
    "room_id": 123,
    "name": "Group Chat Name",
    "type": "group",
    "role": "admin",
    "sticky": false,
    "unread_num": 10,
    "mention_num": 1,
    "mytask_num": 0,
    "message_num": 122,
    "file_num": 10,
    "task_num": 17,
    "icon_path": "https://example.com/ico_group.png",
    "last_update_time": 1298905200
  }
]

配列になっているオブジェクトをマッピングするための RoomEntityクラス を作成します。

public class RoomEntity(roomId: Int, name: String,
                        type: String, role: String,
                        sticky: Boolean,
                        unreadNum: Int,
                        mentionNum: Int,
                        mytaskNum: Int,
                        messageNum: Int,
                        fileNum: Int,
                        taskNum: Int,
                        iconPath: String,
                        lastUpdateTime: Long) {

    public val roomId: Int = roomId
    public val name: String = name
    public val type: String = type
    public val role:String = role
    public val sticky: Boolean = sticky
    public val unreadNum: Int = unreadNum
    public val mentionNum: Int = mentionNum
    public val mytaskNum: Int = mytaskNum
    public val messageNum: Int = messageNum
    public val fileNum: Int = fileNum
    public val taskNum: Int = taskNum
    public val iconPath: String = iconPath
    public val lastUpdateTime: Long = lastUpdateTime
}

全て null を許可しない値で定義してあるので、レスポンスを取得した後の処理では特にnullチェックをすることなく利用することが出来ます。

インターフェースの作成

続いてAPI呼び出しのインターフェースを作成します。

/**
 * Created by komurohiraku on 2015/07/12.
 */
public interface ChatworkService {
    @GET("/rooms")
    public fun getRooms(): Observable<Array<RoomEntity>>
}

https://api.chatwork.com/v1/rooms というパスに対して GET を実行した場合の動作は getRooms() という名前でチャットルーム一覧を取得するメソッドとして定義します。

返却値には先ほど作成した RoomEntity の配列を Observable に包んで指定します。 *2

チャットルーム一覧取得を実行する

表示する Fragment 内で チャットルーム一覧取得 を呼び出す実装を行います。

restAdapter.create(javaClass<ChatworkService>())
                .getRooms()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object:Observer<Array<RoomEntity>>{
                    override fun onCompleted() {
                        // 正常に完了した場合
                    }

                    override fun onError(e: Throwable?) {
                        // エラーの場合
                    }

                    override fun onNext(t: Array<RoomEntity>?) {
                        // 受信した結果
                    }
                })

これでまずはチャットルーム一覧のAPIを呼び出すことができるようになりました。

チャットルーム一覧の結果を加工する

結果のレスポンスは override fun onNext(t: Array<RoomEntity>?) で取得することが出来ます。

Chatwork APIのチャット一覧のレスポンスを確認すると分かるのですが、レスポンスにはチャットルームである グループ個人 が混ざっています。今回は、チャットルームの一覧を取得したいので、 グループ のみをフィルタして結果としましょう。

override fun onNext(t: Array<RoomEntity>?) {
    t?.let {
        val groups = it.filter {
            room -> room.type.equals("group")
        }

        // ランダムで一つの部屋を選択して通知
        val index = Random().nextInt() * 100 % groups.size()
        val room = groups.get(Math.abs(index))
        Toast.makeText(getActivity(), room.name + "が選択されました", Toast.LENGTH_SHORT).show()
    }
}

Kotlinの Arrayfilter利用して type が "group" であるもののみを抽出します。抽出した結果もまた Array です。

チャットルーム一覧取得の機能はこれで概ね完成。全体のコードは以下の通り。

restAdapter.create(javaClass<ChatworkService>())
               .getRooms()
               .subscribeOn(Schedulers.newThread())
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe(object:Observer<Array<RoomEntity>>{
                    override fun onCompleted() {
                            // 正常に完了した場合
                            Snackbar.make(getView(), "ネコと和解せよ!", Snackbar.LENGTH_SHORT).show()
                    }

                    override fun onError(e: Throwable?) {
                            // エラーの場合
                            e?.printStackTrace()
                    }

                    override fun onNext(t: Array<RoomEntity>?) {
                            // 受信した結果
                            t?.let {
                                val groups = it.filter { rooms -> rooms.type.equals("group") }
                               val index = Random().nextInt() * 100 % groups.size()
                               val room = groups.get(Math.abs(index))

                               Toast.makeText(getActivity(), room.name + "が選択されました", Toast.LENGTH_SHORT).show()
                            }
                    }
                })

以上がチャットルーム一覧を取得する方法。

メッセージを投稿する

メッセージを投稿する機能を実装します。チャットに新しいメッセージを追加 こちらを確認すると、APIを POST で呼び出し引数にメッセージとなる文字列を FormUrlEncode に設定すれば良いようです。さっそく作っていきましょう。 *3

レスポンスをマッピングするオブジェクトを定義する

特定のチャットルームにメッセージを投稿する際のレスポンスは、メッセージIDのみが返却されます。

{
  "message_id": 1234
}

そのため、レスポンスオブジェクトは ResponseEntity として以下のように定義します。

public class ResponseEntity(messageId: Int) {
    public val messageId:Int = messageId
}

messageId のみで事足りるため特に問題はないでしょう。 *4

インターフェースの作成

メッセージを投稿する機能のインターフェースを作成します。

@FormUrlEncoded
@POST("/rooms/{room_id}/messages")
public fun postMessage(@Path("room_id") roomId:Int, @Field("body") message: String): Observable<ResponseEntity>

@POST("/rooms/{room_id}/messages") パスの中に {} があります。このように記述するとメソッドの引数を使い、任意の値を埋め込む事ができます。

メソッドの引数でPathに任意の値を埋め込む

@Path("room_id") roomId:Int この引数で指定された値がPathに埋め込まれて実行されます。

URLエンコードを指定する

@FormUrlEncoded このアノテーションの指定と @Field("body")Body で指定された文字列に対して必要になります。

メッセージ投稿を実行する

先ほどと同じく Fragment 内に実装してみます。呼び出し方は先程のチャットルーム一覧取得と変わりません。異なるのは引数に情報が必要であることくらいでしょうか。

private fun postMessage(roomId:Int, message:String, observer: Observer<ResponseEntity>): Subscription {
    return restAdapter.create(javaClass<ChatworkService>())
            .postMessage(roomId, message)
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(observer)
}

一つのプライベートメソッドとして定義してみます。呼び出し方は以下の通り。

postMessage(13768115, "ネコと和解せよ!", object:Observer<ResponseEntity> {
        override fun onCompleted() {
            // 正常に完了した場合
            Toast.makeText(getActivity(), "投稿されましたにゃ", Toast.LENGTH_SHORT).show()
        }

        override fun onNext(t: ResponseEntity?) {
            // レスポンスを受信
            t?.let {
                Log.d("PieceNeko", "MessageID : " + it.messageId)
            }
        }

        override fun onError(e: Throwable?) {
            // エラーの場合
            e?.printStackTrace()
        }
    })

実行結果

実行した結果はこんな感じ

device-2015-08-26-195915

device-2015-08-26-195942

まとめ

Retrofitが便利そうだったのでKotlinで使ってみました。Kotlinらしいコードには程遠いですが、問題なく直感的にサクサク書けます。通信の結果を加工するあたりでは、Kotlinが大いに活躍できるのではないでしょうか?(RxAndroidと機能被る部分もありそうではありますが)

次はこれらの機能を組み合わせてみます。

参照

脚注

  1. 友達がいないからゲームが捗るとかそういう理由ではありません。断じて。多分。
  2. Observableのダイモンド演算子の中にさらにArrayのダイアモンド演算子があって微妙ですが・・・
  3. トトトトトトトト
  4. メッセージIDの取りうる範囲が Int の範囲で良いかは議論の余地がありそうですが。