28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

コルーチンによるUIプログラミングガイド (日本語訳)

Last updated at Posted at 2017-06-14

kotlinx.coroutines v1.4.0 のドキュメント「Guide to UI programming with coroutines」を翻訳しました。

この文書はGitHubで管理し、こちらにはコピーを貼っています。
リンク等はGitHubのほうで見ないと機能しないと思います。


コルーチンによるUIプログラミングガイド

このガイドは、kotlinx.coroutinesのガイドでカバーされている基本的なコルーチンの概念に精通していることを前提としており、UIアプリケーションでコルーチンを使用する方法の具体例を示しています。

すべてのUIアプリケーションライブラリに共通して1つのことがあります。 UIのすべての状態が拘束される単一のメインスレッドがあり、この特定のスレッドでUIに対するすべての更新が行われなければなりません。 コルーチンに関しては、コルーチンの実行をこのメインUIスレッドに制限する適切な コルーチンディスパッチャーコンテキスト が必要であることを意味します。

具体的には、 kotlinx.coroutines は異なるUIアプリケーションライブラリにコルーチンのコンテキストを提供する3つのモジュールを持っています。

また、UIディスパッチャーは、 kotlinx-coroutines-coreDispatchers.Main を介して利用でき、対応する実装(Android、JavaFx、またはSwing)はServiceLoader APIによって検出されます。
たとえば、JavaFxアプリケーションを作成している場合、 Dispatchers.Main または Dispachers.JavaFx 拡張のいずれかを使用できますが、これは同じオブジェクトになります。

このガイドでは、すべてのUIライブラリを同時に扱います。なぜなら、これらのモジュールのそれぞれは、数ページの長さのただ1つだけのオブジェクト定義から成っているからです。 ここには含まれていなくても、これらのどれかを例として使って、好みのUIライブラリ用の対応するコンテキストオブジェクトを書くことができます。

目次

セットアップ

このガイドの実行可能な例は、JavaFx用に提供されています。 利点は、エミュレータなどを必要とせずにすべてのサンプルを任意のOSで直接起動でき、完全に自己完結していることです(各例は1つのファイルにあります)。
Androidでそれらを再現するために必要な変更点(もしあれば)について別途メモがあります。

JavaFx

JavaFxの基本的なアプリケーションの例は、最初に文字列 "Hello World!" を含む hello という名前のテキストラベルと、右下に fab (フローティングアクションボタン)という名前のピンクの円を持つウィンドウで構成されています。

UI example for JavaFx

JavaFXアプリケーションの start 関数は hellofab ノードへの参照を渡す setup 関数を呼び出します。
ここには、このガイドの残りの部分でさまざまなコードを記述しています。

fun setup(hello: Text, fab: Circle) {
    // placeholder
}

ここで完全なコードを取得できます

GitHubのkotlinx.coroutinesプロジェクトをワークステーションにクローンし、IDEでプロジェクトを開くことができます。 このガイドのすべての例は、ui/kotlinx-coroutines-javafxモジュールのテストフォルダにあります。
これにより、各サンプルがどのように動作するかを確認し、変更して実験することができます。

Android

Android StudioでKotlinプロジェクトを作成するには、Getting Started With Android and Kotlinのガイドに従ってください。 アプリケーションにKotlin Android Extensionsを追加することもお勧めします。

Android Studio 2.3では、次のようなアプリケーションが表示されます。

UI example for Android

アプリケーションの context_main.xml に移動し、文字列 "Hello World!"を持ったテキストビューに "hello" というIDを割り当てます。 これは、あなたのアプリケーションで、Kotlin Android拡張機能から hello として利用できるようにするためです。
ピンク色のフローティングアクションボタンは、作成されたプロジェクトテンプレート内で既に fab という名前が付いています。

アプリケーションの MainActivity.kt では fab.setOnClickListener {...} ブロックを削除し、代わりに onCreate 関数の最後の行として setup(hello、fab) 呼び出しを追加します。
ファイルの最後にプレースホルダーの setup 関数を作成します。
ここには、このガイドの以降の部分でさまざまなコードを記述しています。

fun setup(hello: TextView, fab: FloatingActionButton) {
    // placeholder
}

kotlinx-coroutines-android モジュールへの依存関係を app/build.gradle ファイルの dependencies { ... } セクションに追加してください。

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0"

GitHubのkotlinx.coroutinesプロジェクトをワークステーションにクローンすることができます。 Android用のテンプレートプロジェクトは、ui/kotlinx-coroutines-android/example-appディレクトリにあります。
Android Studioで読み込んでAndroidのこのガイドを追試することができます。

基本的なUIコルーチン

このセクションでは、UIアプリケーションでのコルーチンの基本的な使い方を示します。

UIコルーチンの起動

kotlinx-coroutines-javafx モジュールには、JavaFxアプリケーションスレッドにコルーチンの実行をディスパッチするDispatchers.JavaFxディスパッチャーが含まれています。
提示されたすべての例をAndroidに簡単に移植できるように、これを Main としてインポートします。

import kotlinx.coroutines.javafx.JavaFx as Main

メインUIスレッドに限定されたコルーチンは、メインスレッドをブロックすることなく、UI内の何かを自由に更新して中断することができます。
例えば、アニメーションを命令的なスタイルでコーディングして実行することができます。 次のコードは、launch コルーチンビルダーを使用して、1秒に2回、10から1へのカウントダウンでテキストを更新します。

fun setup(hello: Text, fab: Circle) {
    GlobalScope.launch(Dispatchers.Main) { // メインコンテキストでコルーチンを起動
        for (i in 10 downTo 1) { // 10から1までカウントダウン
            hello.text = "Countdown $i ..." // テキストを更新
            delay(500) // 0.5秒待つ
        }
        hello.text = "Done!"
    }
}

ここで完全なコードを取得できます

さて、何が起こりましたか?
メインUIコンテキストでコルーチンを起動しているので、このコルーチン内からUIを自由に更新し、それと同時にdelayなどのサスペンド関数を呼び出すことができます。
delay が待っている間UIスレッドはブロックされないのでUIはフリーズしません。ただ単にコルーチンを中断します。

Androidアプリケーション用のコードも同じです。
setup 関数の本体をAndroidプロジェクトの対応する関数にコピーするだけです。

UIコルーチンのキャンセル

launch 関数が返すJobオブジェクトへの参照を保持し、コルーチンをキャンセルするために使うことができます。
ピンクの円がクリックされたときにコルーチンをキャンセルしましょう。

fun setup(hello: Text, fab: Circle) {
    val job = GlobalScope.launch(Dispatchers.Main) { // メインスレッドでコルーチンを起動
        for (i in 10 downTo 1) { // 10から1までカウントダウン
            hello.text = "Countdown $i ..." // テキストを更新
            delay(500) // 0.5秒待つ
        }
        hello.text = "Done!"
    }
    fab.onMouseClicked = EventHandler { job.cancel() } // クリックされたらコルーチンをキャンセル
}

ここで完全なコードを取得できます

カウントダウンが実行されている間に円がクリックされると、カウントダウンは停止します。
Job.cancelは完全にスレッドセーフでノンブロッキングです。
実際に終了するのを待つことなく、コルーチンがそのジョブをキャンセルするように通知するだけです。 どこからでも呼び出すことができます。
すでに取り消されている、または完了しているコルーチン上でそれを呼び出しても何もしません。

Androidの対応する行は次の通りです。

fab.setOnClickListener { job.cancel() }  // クリックされたらコルーチンをキャンセル

UIコンテキスト内でのアクターの使用

このセクションではUIアプリケーションがUIコンテキスト内でアクターをどのように使用できるかを示し、コルーチンの起動数が無限に増加していないことを確認します。

コルーチンのための拡張

私たちの目標は、この簡単なコードで円がクリックされるたびにカウントダウンアニメーションを実行できるように、 onClickという拡張コルーチンビルダー関数を書くことです。

fun setup(hello: Text, fab: Circle) {
    fab.onClick { // 円がクリックされたらコルーチンを開始する
        for (i in 10 downTo 1) { // 10から1までカウントダウン
            hello.text = "Countdown $i ..." // テキストを更新
            delay(500) // 0.5秒待つ
        }
        hello.text = "Done!"
    }
}

onClick の最初の実装では、各マウスイベントで新しいコルーチンを起動し、対応するマウスイベントを指定されたアクションに渡します(必要な場合のみ)。

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    onMouseClicked = EventHandler { event ->
        GlobalScope.launch(Dispatchers.Main) {
            action(event)
        }
    }
}

ここで完全なコードを取得できます

円がクリックされるたびに新しいコルーチンが始まり、すべてが競合してテキストを更新することに注意してください。
試してみてください。とても良いとは思えません。後で修正します。

Androidでは、対応する拡張を View クラス用に書くことができるので、上に示した setup 関数のコードを変更せずに使うことができます。AndroidのOnClickListenerには MouseEvent は使われていないので省略されています。

fun View.onClick(action: suspend () -> Unit) {
    setOnClickListener {
        GlobalScope.launch(Dispatchers.Main) {
            action()
        }
    }
}

最大で1つの同時ジョブ

新しいジョブを開始する前にアクティブなジョブをキャンセルすることで、多くとも1つのコルーチンだけがカウントダウンを動かしていることを確実にできます。
しかし、それは一般的に最良のアイデアではありません。
cancel関数は、コルーチンを中止するための信号としてのみ機能します。 キャンセルは協調的であり、コルーチンは現時点でキャンセル不可能な何かをしているか、そうでなければキャンセル信号を無視しているかもしれません。 より良い解決策は、同時に実行すべきでないタスクに対してactorを使用することです。
onClick 拡張の実装を変更しましょう。

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    // このノード上のすべてのイベントを処理する1つのアクターを起動する
    val eventActor = GlobalScope.actor<MouseEvent>(Dispatchers.Main) {
        for (event in channel) action(event) // アクションにイベントを渡す
    }
    // このアクターにイベントを提供するリスナーをインストールする
    onMouseClicked = EventHandler { event ->
        eventActor.offer(event)
    }
}

ここで完全なコードを取得できます

アクターのコルーチンと通常のイベントハンドラーの統合の根底にある重要なアイデアは、SendChannelに待機しないoffer関数があることです。
可能ならばアクターにただちに要素を送信し、そうでない場合は要素を破棄します。
offer は実際にはここでは無視している Boolean の結果を返します。

このバージョンのコードで円を繰り返しクリックしてみてください。
カウントダウンアニメーションの実行中は、クリックは無視されます。 これは、アクターがアニメーションで忙しく、そのチャネルから受信しないために発生します。
デフォルトでは、アクターのメールボックスは RendezvousChannel によって支援されています。その offer オペレーションは、receive がアクティブな場合にのみ成功します。

Androidでは、 MouseEvent はありませんので、シグナルとしてアクターに Unit を送ります。
View クラスの対応する拡張は次のようになります。
Androidでは、OnClickListenerに View が送られているので、 View をシグナルとしてアクターに送ります。
View クラスの対応する拡張は次のようになります。

fun View.onClick(action: suspend (View) -> Unit) {
    // 1つのアクターを起動する
    val eventActor = GlobalScope.actor<View>(Dispatchers.Main) {
        for (event in channel) action(event)
    }
    // このアクターをアクティブにするリスナーをインストールする
    setOnClickListener {
        eventActor.offer(it)
    }
}

イベントの合流

以前のイベントを処理している間にイベントを無視するのではなく、最新のイベントを処理する方が適切な場合もあります。
actorコルーチンビルダーは、このアクターがメールボックスに使用しているチャネルの実装を制御する、オプションの capacity パラメーターを受け取ります。
利用可能なすべての選択肢の説明は、Channel()ファクトリ関数のドキュメントに記載されています。

Channel.CONFLATEDの容量値を渡して、統合チャネルを使用するようにコードを変更しましょう。
この変更は、アクターを作成する行にのみ適用されます。

fun Node.onClick(action: suspend (MouseEvent) -> Unit) {
    // このノード上のすべてのイベントを処理する1つのアクターを起動する
    val eventActor = GlobalScope.actor<MouseEvent>(Dispatchers.Main, capacity = Channel.CONFLATED) { // <--- ここを変更
        for (event in channel) action(event) // イベントをアクションに渡す
    }
    // このアクターにイベントを提供するリスナーをインストールする
    onMouseClicked = EventHandler { event ->
        eventActor.offer(event)
    }
}

ここでJavaFxの完全なコードを取得できます。
Androidでは、前の例の val eventActor = ... 行を更新する必要があります。

アニメーションの実行中にサークルをクリックすると、アニメーションの終了後にアニメーションが一度だけ再び起動されます。
アニメーションの実行中に繰り返しクリックされると、合成 して最新のイベントのみが処理されます。

これは、最近受信した更新に基づいてUIを更新することにより、受信する高頻度イベントストリームに対応する必要があるUIアプリケーションにとっても望ましい動作です。
ConflatedChannel を使用するコルーチンは通常、イベントのバッファリングによって発生する遅延を回避します。

上の行で capacity パラメーターを試して、コードの動作にどのように影響するかを調べることができます。
capacity = Channel.UNLIMITED を設定すると、すべてのイベントをバッファーする LinkedListChannel メールボックスを持つコルーチンが作成されます。 この場合、アニメーションは円がクリックされた回数だけ実行されます。

ブロッキング操作

このセクションでは、スレッドをブロックする操作でUIコルーチンを使用する方法について説明します。

UIフリーズ問題

すべてのAPIが実行スレッドを決してブロックしないサスペンド関数として記述されていれば素晴らしいことです。
しかし、ほとんどの場合そうではありません。 場合によっては、例えば呼び出し側スレッドをブロックするようなCPUを消費する計算を行ったり、ネットワークアクセス用のサードパーティAPIを呼び出すだけの場合もあります。
これは、メインUIスレッドをブロックしてUIがフリーズする原因になるため、メインUIスレッドやUI制約コルーチンから直接行うことはできません。

次の例は、この問題を示しています。 メインUIスレッドで最後のクリックを処理するために、前のセクションのUI制約のイベント合流アクターで onClick 拡張を使用します。
この例では、フィボナッチ数の素朴な計算を実行します。

fun fib(x: Int): Int =
    if (x <= 1) x else fib(x - 1) + fib(x - 2)

円がクリックされるたびに、より大きなフィボナッチ数を計算します。
UIのフリーズをより明確にするために、常に実行中の高速カウントアニメーションもあり、メインUIディスパッチャーのテキストを常に更新しています。

fun setup(hello: Text, fab: Circle) {
    var result = "none" // 直前の結果
    // カウントアニメーション
    GlobalScope.launch(Dispatchers.Main) {
        var counter = 0
        while (true) {
            hello.text = "${++counter}: $result"
            delay(100) // 100ミリ秒ごとにテキストを更新する
        }
    }
    // クリックするたびに次のフィボナッチ数を計算する
    var x = 1
    fab.onClick {
        result = "fib($x) = ${fib(x)}"
        x++
    }
}

ここで完全なJavaFxコードを取得できます。
fib 関数と setup 関数の本体をあなたのAndroidプロジェクトにコピーすることができます。

この例の円をクリックしてみてください。
およそ30~40回のクリック後、素朴な計算はかなり遅くなり、UIがフリーズしている間アニメーションが停止するのでメインUIスレッドがフリーズする様子がすぐにわかります。

構造化並列処理、ライフサイクルおよびコルーチンの親子階層

典型的なUIアプリケーションには、ライフサイクルの要素がいくつかあります。
ウィンドウ、UIコントロール、アクティビティ、ビュー、フラグメント、その他の視覚的要素が作成され、破棄されます。
IOまたはバックグラウンド計算を実行する長時間実行コルーチンは、対応するUI要素への参照を必要以上に保持することができ、すでに破棄されて表示されなくなったUIオブジェクトのツリー全体のガベージコレクションを防ぎます。

この問題の自然な解決策は、ライフサイクルを持つ各UIオブジェクトにCoroutineScopeオブジェクトを関連付け、このスコープのコンテキストですべてのコルーチンを作成することです。
簡単にするために、MainScope()ファクトリを使用できます。すべての子コルーチンに Dispatchers.Main と親ジョブを自動的に提供します。

例えば、Androidアプリケーションでは最初に Activity作成 され、不要になったときやメモリを解放しなければならないときに 破棄 されます。
自然な解決策は、 Activity のインスタンスに CoroutineScope のインスタンスを加えることです。

class MainActivity : AppCompatActivity() {
    private val scope = MainScope()

    override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }

    fun asyncShowData() = scope.launch { // アクティビティのスコープを親としてUIコンテキストで呼び出されます
        // 実際の実装
    }

    suspend fun showIOData() {
        val data = withContext(Dispatchers.IO) {
            // バックグラウンドスレッドでデータを計算する
        }
        withContext(Dispatchers.Main) {
            // UIにデータを表示する
        }
    }
}

MainActivity の中から起動したコルーチンはすべてそのジョブを親として持ち、アクティビティが破棄されると直ちに取り消されます。

Androidは、ライフサイクルを持つすべてのエンティティでコルーチンスコープをファーストパーティでサポートしていることに注意してください。
対応するドキュメントを参照してください。

ジョブ間の親子関係は階層を形成します。
アクティビティに代わってバックグラウンドジョブを実行するコルーチンは、さらに子コルーチンを作成できます。
親ジョブがキャンセルされると、コルーチンのツリー全体がキャンセルされます。
その例は、コルーチンのガイドの「コルーチンの子」セクションに示されています。

ブロッキング操作

メインUIスレッドでのブロッキング操作の修正は、コルーチンでは非常に簡単です。
「ブロックキング」 fib 関数をノンブロッキングサスペンド関数に変換します。このサスペンド関数は、withContext関数を使用して実行コンテキストをバックグラウンドのスレッドプールによって支えられたDispatchers.Defaultに変更して計算を実行します。
fib 関数は suspend 修飾子でマークされていることに注意してください。
これは、呼び出されたコルーチンをブロックしませんが、バックグラウンドスレッドの計算が動作しているときにその実行を中断します。

suspend fun fib(x: Int): Int = withContext(Dispatchers.Default) {
    if (x <= 1) x else fib(x - 1) + fib(x - 2)
}

ここで完全なコードを取得できます。

このコードを実行して、大きなフィボナッチ数が計算されている間、UIがフリーズしていないことを確認できます。
しかし、このコードは fib をいくらか遅く計算します。なぜなら、 fib へのすべての再帰呼び出しは withContext を経由するからです。
これは、実際には大きな問題ではありません。なぜなら、 withContext はコルーチンがすでに必要なコンテキストで実行されているかどうかを確認するのに十分スマートで、別のスレッドにコルーチンを再ディスパッチするオーバーヘッドを避けるからです。
それにもかかわらず、 withContext の呼び出しの間に整数を追加する他に何もしないこのプリミティブコードでは明らかにオーバーヘッドがあります。
より実用的なコードでは、余分な withContext 呼び出しのオーバーヘッドは重要ではありません。

それでも、元の fib 関数の名前を fibBlocking に変更し、 fibfibBlocking の上に withContext ラッパーを被せて定義してバックグラウンドスレッドで動かせば、この fib の実装は以前のように高速で実行できます。

suspend fun fib(x: Int): Int = withContext(Dispatchers.Default) {
    fibBlocking(x)
}

fun fibBlocking(x: Int): Int =
    if (x <= 1) x else fibBlocking(x - 1) + fibBlocking(x - 2)

ここで完全なコードを取得できます。

メインUIスレッドをブロックせずに、フルスピードのフィボナッチ計算を楽しむことができます。
必要なのは、 withContext(Dispatchers.Default) だけです。

fib 関数はコード内の単一のアクターから呼び出されるので、与えられた時間に最大で同時に1つの計算をすることに注意してください。したがって、このコードはリソース使用率に自然な制限があります。
最大で1つのCPUコアを飽和させることができます。

高度なトピック

このセクションでは、さまざまな高度なトピックについて説明します。

ディスパッチせずにUIイベントハンドラーでコルーチンを開始する

UIスレッドからコルーチンが起動したときの実行順序を視覚化するために setup に次のコードを書きましょう。

fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        GlobalScope.launch(Dispatchers.Main) {
            println("Inside coroutine")
            delay(100)
            println("After delay")
        }
        println("After launch")
    }
}

ここで完全なJavaFxコードを取得できます。

このコードを開始しピンクの円をクリックすると、次のメッセージがコンソールに出力されます。

Before launch
After launch
Inside coroutine
After delay

ご覧のように、launchの後すぐに実行が継続され、後で実行するためにコルーチンがメインUIスレッドにポストされます。
kotlinx.coroutines のすべてのUIディスパッチャーはこのように実装されています。
なぜでしょうか?

基本的にここでの選択は、「JSスタイル」の非同期アプローチ(非同期アクションは常にイベントディスパッチスレッドで後から実行されるように延期されます)と「C#スタイル」アプローチ(非同期アクションは、最初の中断ポイントまで呼び出し元スレッドで実行されます)の間です。

C#のアプローチはより効率的であるように見えますが、「必要な場合は yield を使用する…」などの推奨事項があります。
これはエラーが起こりやすいです。
JSスタイルのアプローチはより一貫性があり、プログラマーはyieldする必要があるかどうかについて考える必要はありません。

しかし、この特定のケースでは、コルーチンがイベントハンドラーから開始されそのまわりに他のコードがない場合、この追加のディスパッチは実際には付加価値を持たず余分なオーバーヘッドを追加します。
この場合、launchasyncおよびactorコルーチンビルダーに対するオプションのCoroutineStartパラメーターを使用して、パフォーマンスを最適化することができます。
これをCoroutineStart.UNDISPATCHEDの値に設定すると、次の例に示すように最初の中断ポイントまですぐにコルーチンを実行し始める効果があります。

fun setup(hello: Text, fab: Circle) {
    fab.onMouseClicked = EventHandler {
        println("Before launch")
        GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { // <--- この変更に注意
            println("Inside coroutine")
            delay(100)                            // <--- そしてここがコルーチンが中断する場所
            println("After delay")
        }
        println("After launch")
    }
}

ここで完全なJavaFxコードを取得できます。

クリックすると次のメッセージをプリントします。コルーチンのコードの実行がすぐに開始されることを確認してください。

Before launch
Inside coroutine
After launch
After delay
28
27
0

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
  3. You can use dark theme
What you can do with signing up
28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?