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

2015.08.27

こんにちは。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(), 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 = ""
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>
}

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

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

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

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

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

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

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

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

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

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

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

override fun onNext(t: Array?) {
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())
.getRooms()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object:Observer<Array>{
override fun onCompleted() {
// 正常に完了した場合
Snackbar.make(getView(), "ネコと和解せよ!", Snackbar.LENGTH_SHORT).show()
}

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

override fun onNext(t: Array?) {
// 受信した結果
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

@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): Subscription {
return restAdapter.create(javaClass())
.postMessage(roomId, message)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer)
}

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

postMessage(13768115, "ネコと和解せよ!", object:Observer {
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 の範囲で良いかは議論の余地がありそうですが。