読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

mastodon4j v0.0.6 をリリースしました

mastodon4jをリリースしました - visible true では0.0.3でしたが、0.0.6まで出ました。

差分はReleases · sys1yagi/mastodon4j · GitHubに書いてますがここでも軽く書きます。

v0.0.4

Release v0.0.4 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • 各メソッドにContractを追加。PublicとAuthRequiredに分かれている。
    • 認証が必要なものと不要なものを明示するのが目的だったが、interfaceは@JvmOverloadsが使えないため0.0.7でやめる予定
  • Breaking メソッドの各関数にMastodon4jRequestExceptionのチェック例外を付与
  • Breaking booleanのgetter名をisXXXに変更
  • いくつかの関数に@JvmOverloadsを付与

mastodon4j-rx

なし

v0.0.5

Release v0.0.5 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのpublicとtagにlocalパラメータを追加。これをつけるか付けないかでローカルタイムライン、連合タイムラインを切り替えるらしい。それに伴い次の変更を入れた
    • Deprecated Timelines#getPublic()
    • Add Timelines#getLocalPublic()
    • Add Timelines#getFederatedPublic()
    • Deprecated Timelines#getTag()
    • Add Timelines#getLocalTag()
    • Add Timelines#getFederatedTag()

mastodon4j-rx

なし

v0.0.6

Release v0.0.6 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのlocalパラメータは、falseの時はパラメータを付与してはいけないという事で修正
  • Accounts.getStatusesにonly_mediaフラグを追加。メディアを持つ投稿を抽出する時に使うっぽい
  • 次のDeprecated関数を削除
    • Delete Timelines#getTag()
    • Delete Timelines#getPublic()
  • Link Headerをサポート。そのためにList<T>の代わりにPageable<T>Linkを導入

mastodon4j-rx

mastodon4jの変更に追従

その後

  • 0.0.7 Milestone · GitHub
    • raw jsonを取り出す仕組みを入れる
    • Mastodon4jRequestExceptionはResponseを内包しているけど、Mastodon4jRequestException自身にcode()とかを持たせる
    • Contractを捨ててアクセストークンが不要なメソッドはPublicクラスに集約する
    • できればStreaming APIをサポートする

所感

PR投げてくれる人がいたり(fix only_media param by takke · Pull Request #39 · sys1yagi/mastodon4j · GitHub)、

設計周りでアドバイスを頂けたりしてありがたいです。

mastodon4jをリリースしました

mastodon4jをリリースしました。Kotlinで書かれていて、Javaからでも使えるように今後チューニングしていきます。現在の最新は0.0.3です。 最初はDroiDonの副産物としてmastodon4jを実装していて、まぁだれか出すだろうと思っていたけど1週間経っても出てこないので自分で出すことにしました。 公式ドキュメントのAvailable librariesに載せてもらえてやっぴー。

github.com

mastodon4j

github.com

0.0.1

  • mastodon4jでmastodonAPI documentにかかれているデータ、メソッドをすべて実装
  • mastodon4j-rxで一部のメソッドを実装

0.0.2

Release v0.0.2 · sys1yagi/mastodon4j · GitHub

  • ユーザ名/パスワードで認証する Apps#postUserNameAndPassword() を追加
  • Statues内でRangeを利用するようにした。(0.0.1でもRangeはあったが、Statuesではmax_id, since_id, limitを個別のパラメータにしていた)
  • mastodon4j-rxですべてのメソッドを実装

0.0.3

Release v0.0.3 · sys1yagi/mastodon4j · GitHub

  • Mastodon4jRequestExceptionでResponseオブジェクトを持つようにした(401などのハンドリングのため)。
  • Scopeのコンストラクタが可変長引数で、空のまま実行するとエラーになるのでデフォルト引数を追加した

その後

所感

mastodon4jを実装して改めて思ったのはAndroidアプリケーションの開発って大変だなーということ。DroiDonの進捗はせいぜい5%くらいでまだまだ先は長い。

github.com

React NativeでFirebase Storageに画像を上げるときにputStringが上手くいかないのでbase-64を入れてatob関数を補完する

React Nativeを、ペーパープロトタイピングprottなどを使ったモックアッププロトタイピングの次のフェーズとして動くプロトタイピングツールに使えないかなぁと思ってちょこちょこ触ってます。

うまくいくと両ユーザ向けに同時に動くプロトタイプを提供できてフィードバックが捗るのとAndroidiOSの両方のチームで同時に一つの仕様をいじれるので仮説や価値の理解や共有らなんやら色々捗るんじゃないかなぁとか思ってます。

バックエンド側もFirebaseを使うと結構カジュアルに色々やれそうだな〜とか思っていて色々試し始めたんですがFirebase Storageにデバイス上の画像をアップロードする処理ではまりました。

環境

  • React Native: 0.42.3
  • Firebase: 3.7.3

問題

react-native-image-pickerなんかを使って写真を撮ったりデバイス上の写真を選択すると、ファイル名やContentTypeやbase64に変換された実データを取得できます。次のコードはFirebase StorageにputString関数で画像をアップロードする例です。

firebase.initializeApp(config);
const storage = firebase.storage().ref();

const ref = storage.child(response.fileName);
const metadata = {contentType: response.type};

ref.putString(response.data, 'base64', metadata).then((snapshot) => {
  done();
});

putString関数の第一引数のresponse.dataには画像をbase64に変換した文字列が詰まっており、第二引数はデータフォーマットを示すためにbase64を渡しています。これをReact Nativeで実行すると次のようにInvalid character foundとなります。

f:id:sys1yagi:20170326215741p:plain:w300

どーもよくわからないのでBlobやUint8Arrayを使う方法を試みるも、そもそもReact NativeにはBlobはないらしいという事がわかり、react-native-fetch-blobというBlob周りのポリフィルを提供しているライブラリを導入して試してみるもうまくいかず途方にくれてました。

原因

万策尽きたのでしぶしぶFirebase Storageのクライアントコードを読むことにしました。エラー画面にstacktraceが吐かれてるのでそんなに追いかけるのは難しくなかったです。

f:id:sys1yagi:20170326220926p:plain

こんな感じでtry-catchがあって、atob関数しか呼び出してないのでこれがちゃんと動いてないんだな〜という事がわかりました。

対応

Blobを使ったりするところであれこれ試す中で、

javascript - How to convert base64 into Blob in React Native? - Stack Overflow

とかを試していたのでピンときて、

npm install --save base-64

して(base64)、

const atob = require('base-64').decode;
window.atob = atob;

コンポーネントの冒頭に書いたらうごいた。わいわい。

f:id:sys1yagi:20170326221919p:plain

雑感

React Nativeのポリフィル集ありそうだけどどーなんだろ。 React.parts – A catalog of React componentsとかを眺めていると結構色々あって面白い。一方で改廃も激しいので大変そう。

Kotlin 1.1で追加された標準ライブラリの関数をざっくり見る

Kotlin 1.1でいくつか標準ライブラリに追加された関数があるのでちら見しながら感想を述べます。

Kotlin 1.1: What’s coming in the standard library | Kotlin Blog

T.also()

let(), apply(), with(), run()に新たな仲間が。

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

内容としてはlet()の型1つ版って感じです。let()は引数と戻り値の型が別の定義になっていたのでラムダ式の末尾に戻り値を書かないといけませんでした。also()では引数が戻り値になるのでそうした対応が不要になります。

applyに近いですが、applyはラムダ式のスコープがレシーバとなるため呼び出し元のthisとの衝突が起こります。alsoはラムダ式のスコープを汚さずにapplyに近い処理を書けるようになります。

用途としては次のような感じになると思います。

val binding = HogeBinding.inflate(inflator).also {
  it.root.setOnClickListener(this) // thisのスコープが外側のクラスになる
}

T.takeIf()とT.takeUnless()

グローバルな拡張関数が2つ。ある条件を満たした時|満たさない時自身を返す。

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = if (predicate(this)) this else null
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.takeUnless(predicate: (T) -> Boolean): T? = if (!predicate(this)) this else null

用途としてはこんな感じかな〜

val params = request.takeIf{ isValidParameter(it) }.params ?: throw RuntimeException()

わりと便利。

Iterable.groupingBy()

groupBy()はリストをkey selectorでグルーピングしてMap<key, list>に変換する。一方でgroupingBy()はkey selectorでグルーピングした結果を次のようにGrouping<T>で返す。

@SinceKotlin("1.1")
public inline fun <T, K> Iterable<T>.groupingBy(crossinline keySelector: (T) -> K): Grouping<T, K> {
    return object : Grouping<T, K> {
        override fun sourceIterator(): Iterator<T> = this@groupingBy.iterator()
        override fun keyOf(element: T): K = keySelector(element)
    }
}

Grouping<T>はfold関数, reduce関数, eachCount関数を持っていて、key selectorでグルーピングしたリストに対してそれぞれの処理を行える。

listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
  .groupingBy { it % 2 == 0 }
  .fold(0, { a, b ->
    a + b
  }) // Map<key, list>を返す
  .forEach { t, u ->
    println("$t=$u") // false=25, true=20
  }

使いみちはなんか出現文字の頻度を数えるとからしいけどうーんて感じた。mapとかあると便利そうなんだけどな〜と思った。

minOf() and maxOf()

minOf()とmaxOf()はトップレベル関数として定義されている。引数を比較して最小、最大を返す。引数は2つと3つがある。次のコードはIntの実装だがプリミティブ型毎に実装があるほかT型の実装もある

@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun minOf(a: Int, b: Int): Int {
    return Math.min(a, b)
}

T型は次の通り。TはComparableである必要がある。

@SinceKotlin("1.1")
public fun <T: Comparable<T>> maxOf(a: T, b: T): T {
    return if (a >= b) a else b
}

引数の最後にComparator<in T>を受け取るバージョンもある。

@SinceKotlin("1.1")
public fun <T> maxOf(a: T, b: T, comparator: Comparator<in T>): T {
    return if (comparator.compare(a, b) >= 0) a else b
}

Collectionには合ったけどカジュアルに比較するやつがないから追加されたのかな。地味に便利ではありそう。

Map.getValue()

Mapのget関数の戻り値はV?であり、keyがない場合はnullを返す。一方で今回追加されたgetValue関数はVを返す。もしもkeyがない場合はNoSuchElementExceptionをスローする。

@SinceKotlin("1.1")
public fun <K, V> Map<K, V>.getValue(key: K): V = getOrImplicitDefault(key)

get関数の戻り値がnull許容型で利用時にめんどくさいってのがあるのでよさそう。 withDefaultをあわせて使えばNoSuchElementExceptionはなくなるがまぁそれならnull許容型使うのと同じかな。

mapOf(
  1 to "a",
  2 to "b"
)
.withDefault { "c" }
.getValue(100) // "c"

Map.minus() operator

Mapにマイナスオペレータが追加された。

var a = mapOf(
                1 to "a",
                2 to "b"
        )
a += Pair(3, "c") // 1="a", 2="b", 3="c"
a -= 1 // 2="b", 3="c"

なるほどな〜。実装としてはこんな感じ。

@SinceKotlin("1.1")
public operator fun <K, V> Map<out K, V>.minus(key: K): Map<K, V>
        = this.toMutableMap().apply { minusAssign(key) }.optimizeReadOnlyMap()

@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline operator fun <K, V> MutableMap<K, V>.minusAssign(key: K) {
    remove(key)
}

Array-like List instantiation functions

Array型と同様にコンストラクタに要素数を指定して、各インデックスの値の初期化をラムダ式で記述できるようになった。

List(3) { i -> "value$i" } // "value0","value1","value2"
MutableList(3) { i -> "value$i" }

使う機会はパッとは思いつかないけどまぁよさそう。

String to number conversions

StringのtoInt関数などで失敗した場合NumberFormatExceptionをスローするが、新たにtoIntOrNull関数が追加された。

@SinceKotlin("1.1")
public fun String.toIntOrNull(): Int? = toIntOrNull(radix = 10)

このほかtoDoubleOrNull関数やtoFloatOrNull関数などがある。try-catchいちいち書かなくていいので便利そう。

Iterable.onEach()

forEach()は戻り値がUnitだけど、onEachは自身を返す。

@SinceKotlin("1.1")
public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {
    return apply { for (element in this) action(element) }
}

途中にデバッグ用に出力するとかかな〜〜〜。

return listOf(1, 2, 3)
  .onEach {
    println(it) // 1, 2, 3
  }
  .map(Int::toString) // ["1", "2", "3"]

Map.toMap() and Map.toMutableMap()

Mapにコピー関数であるtoMap関数とtoMutableMap関数が追加された。

@SinceKotlin("1.1")
public fun <K, V> Map<out K, V>.toMap(): Map<K, V> = when (size) {
    0 -> emptyMap()
    1 -> toSingletonMap()
    else -> toMutableMap()
}

内部的にはLinkedHashMapが作られるみたい。

@SinceKotlin("1.1")
public fun <K, V> Map<out K, V>.toMutableMap(): MutableMap<K, V> = LinkedHashMap(this)

Abstract collections

読み取り専用としてAbstractCollection、AbstractList、AbstractSet、AbstractMapが、変更可能な値としてAbstractMutableCollection、AbstractMutableList、AbstractMutableSet、AbstractMutableMapが追加された。抽象クラスを継承して独自のコレクションクラスを実装できるようになった。今までなかったのか〜

雑感

also()とtakeIf()はガンガン使っている。めっちゃ便利。どんどん便利になってよい〜

DroidKaigi 2017で「解剖Kotlin ~バイトコードを読み解く~」を発表してきました

DroidKaigi 2017 3/9 15:10~ Room4で発表しました。

発表の内容

KotlinはJavaという話です。

感想

それなりに練習したけどわりと噛んだりつっかえたりしたのでもっと練習が必要だな〜と感じた。内容については前日までかなり色々迷って紆余曲折あったけど結果的に、Kotlinにそこまで詳しくない層に対してKotlinはそんなに難しくないということが伝えられる内容になったんじゃないかなと思う。

KPT

K

  • 今回は前回ほど準備で焦らなかったのでよかった。テーマ設定がわりと明確だったのが良かったんだと思う
  • 前日のパーティで励ましあえてよかった。不安すぎて「こういうこと話そうと思ってるんですよね〜」とか話しかけまくって「おーおもしろそうですね」みたいな反応を収集して気持ちを落ち着けてた。次回は主に励ます側に回りたい。
  • 直前はナーバスになったけど生活サイクルに乱れはそこまで生じなかったのはよかった。

P

  • もっと早く着手すべきだった(たしか去年も思った)。完成がギリギリだった。
  • テーマは初期から明確だったけどかなり内容が決まるまで迷走した。ターゲットが誰かについてもっと明確に定義しておくべきだった(途中で掘り下げを深めようとしたけど詳しくない人には意味不明になるので危なかった)。
  • 自分の発表までは他のセッションを聴く精神的な余裕がなかった
  • タイトルについてはもうちょい実体に即した形にすればよかったかなと思った。イメージしていたものと違うと感じた人もいると思う。ネタの発想時点(2016年4,5月あたり)ではバイトコードしか見られなかったからこういうタイトルだった

T

  • 来年はDroidKaigi自体のテーマ曲とかなんか作りたい。ウェルカムトークとかで流す感じのやつ
  • 来年は発表前でも他の人のセッション聞きに行きたい
  • 超早めに資料を完成させる(最低でも2週間前に完成)

Androidアプリケーションのビルド体験を改善するリモートビルドサービス Cyborg-Build を作りました

はじめに

これはAndroid Advent Calendar 2016の20日目のエントリです。

本エントリではAndroidアプリケーションのビルド体験を改善するリモートビルドサービス Cyborg Build を紹介します。

背景

以前AndroidアプリケーションをGoogle Compute Engineのインスタンスでビルドする - visible trueクラウド上にインスタンスを作ってAndroidアプリケーションのビルドを代行させる方法について書きました。エントリでは低スペックなインスタンスでの試用でしたが、その後いくつかのインスタンスを試しGCEではn1-highcpu-8(vCPU x 8、メモリ 7.2 GB)くらいのスペックがパフォーマンスとコストのバランスが良いとわかりました*1

パフォーマンスとコストのバランスが良いと言ってもn1-highcpu-8は一ヶ月フル稼働で$150ほどかかります。インスタンスの可用性の設定をプリエンプティブにすれば$44くらいまで抑えられますが、プリエンプティブインスタンスは最長で24時間の稼働という制限があるので、インスタンスの起動処理などを別途行う必要があります。さらに最長で24時間といっても5分で落ちるケースもあるので安定的な動作をさせたい場合は色々と工夫が必要になります。

gcloudなどを使ってインスタンスの状態を見て起動したり落としたりするのを自動化してもよかったのですが、この辺りで「もっと簡単に誰でも使えて安上がりな仕組みができないか」と考えてサービス化を検討し始めました。

解決したい課題

リモートビルドサービスで解決したい課題は次の通りです。

  1. ビルドを待つ間CPUを専有されてしまって他の作業ができない
  2. ビルド速度が遅い
  3. リモートビルドシステムの構築やメンテの手間がかかる
  4. 運用コストが高い

1と2はAndroidアプリケーション開発における課題で、3と4はリモートビルドシステムを自前で運用する場合の課題です。

今回作ったCyborg Buildは1,3を解決しています。2についてはリモートインスタンスのスペックアップで可能ですが、4の問題をサービス化で解決できるかどうかを先に検証しなければならないので直近のスコープには入っていません。

Cyborg-Build

1ヶ月ほどプロトタイピングしながらあれこれ試してある程度形になってきたので本日よりα版として公開します。ビルド時の体験が劇的に良くなると思うのでぜひ使ってみてください。

Cyborg Build

始め方はこちら

http://cyborg-build.com/getstarted

仕組み

サービスの仕組みは次の通りです。

Cyborg-BuildにGoogleアカウントでログインするとGoogle Cloud Storageのバケットをアカウントごとに払い出します。このバケットにソースコードをアップロードします。バケットにはログインしたアカウントとCyborg-Buildサービスのみがアクセスできます。

Cyborg-Build内でビルドコマンドを定義したプロジェクトを作成すると上図の操作をするスクリプトをダウンロードできます。ビルドスクリプトAndroidプロジェクトのルートに置いて実行するとバケットへソースコードをアップロードし、Cyborg-Buildに対してビルドリクエストを行います。Cyborg-Buildは対応するリクエストをリモートインスタンスで実行し、成果物をバケットにアップロードします。その後ビルドスクリプトはアップロードされた成果物をローカルにダウンロードします。オプションで起動アクティビティを設定すれば端末へのインストールと実行まで行えます。

機能

  • プロジェクトの作成
    • プロジェクトはパッケージ名と複数のタスクを持っています。
    • タスクは実行するgradleタスク回収する成果物のパス起動するアクティビティ名を設定できます
  • リモートビルドスクリプトのダウンロード
    • 作成したプロジェクトにもとづいてリモートビルドスクリプトを生成します
  • コマンドラインでのリモートビルドの実行

制限

α版なので最低限リモートビルドが体験できるところまでの機能となっています。次の項目はまだサポートしていませんが順次改善していきます。

  • 作成したプロジェクトの編集(かなり早期にやります)
  • イントラ内にあるmaven repositoryへのアクセス(未検討)
  • NDKの利用(おそらく動くとは思いますが...)
  • gradlewタスク以外のコマンド実行
  • テスト結果などの回収(動くかまだ試してない)
  • 各種メトリクスの表示などなど
  • IDEA Plugin化によるシームレスなリモートビルドの利用

α版で検証すること

色々とやりたい事はあるのですがα版では次の点に絞って取り組もうと思ってます。

  1. そもそもリモートビルドに価値があるのかを検証する
  2. 自己運用より安価にリモートビルドを提供ができるか
  3. 十分なマーケットサイズがあるか

資金周りが最初からクライマックスなので2については早期に検討して課金を開始したいところです。

フィードバックをください

個人的にはリモートビルドほんとに最高なのでぜひ皆さんに使っていただきたいです。そして色々とフィードバックを頂けると嬉しいです。

  • Twitter: @sys1yagi
    • おそらく一番反応が良いです。
  • Slack: invite link
    • 一応常駐しています。
  • ご意見フォーム(Cyborg Build)
    • トップの右下に設置しています。投稿は必ず読みます

サイトは英語ですがわりと適当なのでtypoや文法違うぞといったフィードバックももらえると嬉しいです。この構成だと動かない!みたいなケースもslackなどで相談して頂けると応対できると思います。

おわりに

個人のアプリはすでにCyborg-Buildを使って開発しています。会社ではリモートビルドシステムが構築済なのでそちらを使ってますが、コスト面でCyborg-Buildのほうが安いよねって感じにして移行できたらな〜とか思っています。とりあえず世界中のAndroidエンジニアのビルド体験を最高にできたらいいなと思っています。

*1:AWSではc3.2xlarge辺りを使っています

Kotlin 1.1 async/awaitの仕組みと使い方の概要 for Android

これはKotlin Advent Calendar2016の19日目のエントリです。

本エントリではKotlinの次期バージョン(1.1)で導入されるコルーチンと、その実装のひとつであるasync/awaitについて解説します。

今回書いているコードはGitHub - sys1yagi/kotlin-async-await-sample: yey!に置いています。

Kotlin 1.1の様子

Kotin 1.1は2016年7月にFirst glimpse of Kotlin 1.1: Coroutines, Type aliases and moreで変更の概要とEAPが公開されました。コルーチンのほかにタイプエイリアスやメソッド参照、ラムダ式での引数の分解宣言などなど様々な便利な機能の追加が予定されています。2016年12月の時点で1.1-M03が公開されています。

Kotlin 1.1の仕様はKEEPで検討が進められていて、最近では1.1以降のものについても検討が走っているようです。Github ProjectsのKEEP Kotlin 1.1 では1.1に絞ったissueの進捗を確認できます。

コルーチンとはなにか

コルーチンは任意の箇所で一時停止ができる計算と考えることができます。一般的な関数は処理の開始からリターンまででひとつの処理を表しますが、コルーチンは処理の途中に中断するポイントを明示できます。中断するごとに値を返せるほか、中断したときの状態を保ったまま任意のタイミングで再開ができるので、非同期処理や遅延リストの作成(ジェネレータ)を簡潔に書けるようになります。

次のコードはasync/awaitを用いた非同期処理の待ち合わせの例です。

asyncAndroid {
  val user = userApiClient.get(id).await() // ここで中断して非同期処理
  val articles = articleApiClient.getArticles(user).await() // ここで中断して非同期処理
  // 取得完了
  showArticles(articles)
}

一般的な非同期処理はコールバックを用いてネストが深くなりますが、コルーチンを用いるとフラットに記述できるようになります。

Kotlin 1.1のコルーチンのアプローチ

Kotlin 1.1ではコルーチンの処理全体を「状態」の集まりとしてステートマシンに変換することで、JVM上での動作を実現しています。

コルーチンのコードをステートマシンとして解釈するために、コルーチンの処理を表すcoroutineキーワードと、中断点を示すためのsuspendキーワードが追加されています。これらのキーワードによって「コルーチンの実装」を言語機能としてサポートします。

Kotlin 1.1で提供されるasync/awaitやジェネレータは、コルーチンの実装としてライブラリの形式で提供されます。

Kotlin 1.1におけるコルーチンの実現方法の詳細についてはTechBoosterがC91で頒布する「なないろAndroid」に詳細な解説を書いていますのでぜひ読んでみてください。

Kotlin 1.1を導入する

Kotlin 1.1はまだEAPの段階なので導入のためにいくつか設定が必要です。次のようにEAPmaven urlを追加した上で1.1-M03などのバージョンを指定してください。差分のみ記載しているので適宜必要なものを設定してください。

// build.gradle
buildscript {
  ext.kotlin_version = '1.1-M03'
  repositories {
    maven { url = 'https://dl.bintray.com/kotlin/kotlin-eap-1.1' }
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}
repositories {
  maven { url = "https://dl.bintray.com/kotlin/kotlin-eap-1.1" }
}

// module/build.gradle
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

dependencies {
  compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

Koltin PluginをEAP 1.1にする

Android Studio向けのKotlin PluginもEAP版が提供されています。[Tools]-[Kotlin]-[Configure Kotlin Plugin Updates]でUpdate channelを'Early Access Preview 1.1'に変更し、プラグインをアップデートしてください。

async/awaitライブラリを導入する

Kotlin 1.1向けのコルーチン実装はkotlinx.coroutinesで開発されています。現時点では次の3つがbintrayで配布されています。

  • kotlinx-coroutines-generate : ジェネレータ
  • kotlinx-coroutines-async : async/await
  • kotlinx-coroutines-rx : RxJavaを用いたasync/await

kotlinx-coroutines-asyncはasync/awaitのインタフェースとしてCompletableFutureを用いています。CompletableFutureはJava 8のクラスなので、Androidで利用しようとするとminSdkVersion 24が必要となります。そのためAndroidでasync/awaitを使う場合はkotlinx-coroutines-rxを選択することになります。

kotlinx-coroutines-rxを導入する設定は次の通りです。スレッド操作のためにrxbinding-kotlinも入れておきます。

buildscript {
  ext.kotlin_coroutines_version = '0.1-alpha-2'
}
repositories {
  maven { url = "https://dl.bintray.com/kotlin/kotlinx.coroutines" }
}
dependencies {
  compile "org.jetbrains.kotlinx:kotlinx-coroutines-rx:$kotlin_coroutines_version"
  compile('com.jakewharton.rxbinding:rxbinding-kotlin:1.0.0') {
    exclude group: 'io.reactivex', module: 'rxjava'
    exclude group: 'com.android.support', 'module': 'support-annotations'
    exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
  }
}

Androidでasync/awaitを使う

kotlinx-coroutines-rxはasyncRx関数を提供します。asyncRx関数は引数のブロック内で呼び出した中断関数を直列に連結し、最後の処理の値を戻り値としてObservableで包みます。

val observable = asyncRx<List<Article>> {
    val user = userApiClient.get(1L).awaitSingle() // ここで中断と再開をする
    articleApiClient.getArticles(user).awaitSingle() // ここで中断し、戻り値を返す
}
observable.subscribe { articles ->
    // do something.
  }

上記の例ではawaitSingle()が中断関数です。この関数はObservableの拡張関数として定義されています。このほかにawaitFirstawaitLastがあります。中断関数の種類や名前はコルーチンの実装に依存します。

asyncRx関数の機能は、Observableの中断関数の呼び出しにもとづいて直列に処理を連結することのみで、実行スレッドの管理等は行ってくれません。非同期処理をするために次のように書く必要があります。

val observable = asyncRx<List<Article>> {
  val user = userApiClient.get(1L)
    .subscribeOn(Schedulers.io())
    .awaitSingle()
  articleApiClient.getArticles(user).awaitSingle()
}
observable
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe { articles ->
      // do something.
  }

かなり不格好ですね。このまま実用するのはちょっと冗長に思えます。というか普通にRxJavaを使えばいいような気がしますね。

asyncAndroidを実装する

asyncRxをそのまま使ってもあまり便利ではないので、次の要件を満たす独自のコルーチンとしてasyncAndroid関数を実装してみましょう。

  • Observableは常にsubscribeOn(Schedulers.io())observeOn(AndroidSchedulers.mainThread())で実行する
  • コルーチンの中で非同期、同期処理のすべてを記述できる

実装内容が次です。

package kotlinx.coroutines.android

import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription

fun asyncAndroid(
  coroutine c: RxAndroidController.() -> Continuation<Unit>
): CompositeSubscription {
  val subscriptions = CompositeSubscription()
  val controller = RxAndroidController(subscriptions)
  c(controller).resume(Unit)
  return subscriptions
}

class RxAndroidController internal
                        constructor(val subscriptions: CompositeSubscription) {
  suspend fun <T> Observable<T>.awaitSingle(x: Continuation<T>) {
    this.single()
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribeWithContinuation(x)
  }

  private fun <T> Observable<T>.subscribeWithContinuation(x: Continuation<T>) {
    val subscription = subscribe(x::resume, x::resumeWithException)
    subscriptions.add(subscription)
  }
}

このコードの説明は省略します。内容を読み解くにはTechBoosterがC91で頒布する「なないろAndroid」をぜひ参照してください。あるいはkotlin-coroutines-informal.mdを読むのもいいかもしれません。

asyncAndroid関数を使うと次のような記述ができるようになります。便利ですね。

asyncAndroid {
  val user = userApiClient.get(1L).awaitSingle() // 中断して非同期実行
  // UIスレッドで再開
  binding.textArea.text = "asyncAndroid loading articles..." 
  val articles = articleApiClient.getArticles(user).awaitSingle() // 中断して非同期実行
  // UIスレッドで再開
  binding.textArea.text = "asyncAndroid finish loading. size=${articles.size}"
}

おわりに

筆者はすでに個人のプロダクトでEAPのKotlin 1.1を導入しています。特にasync/awaitとタイプエイリアスとメソッド参照が便利です。正式版が楽しみですね。Kotlinサイコ-なのでぜひ皆さんも使ってください。

あわせて読みたい:コミックマーケット91 新刊情報と予約開始 | TechBooster