Kotlinでコルーチンをはじめよう【Androidアプリ開発】
コルーチン (Coroutine) とは、Kotlin で並列処理を非同期におこなえる軽量なスレッドです。Android アプリ開発でもコルーチンを使えるので、今までやっていた非同期処理を、Kotlin のコルーチンに変えてみようと思いました。本記事では、Android開発におけるコルーチンの使い方を理解できている範囲でまとめました。
▼ こちらの書籍でもCoroutineの使い方が解説されてます。
非同期処理のコルーチンを可能にするライブラリのインストール
Androidアプリでコルーチンを使えるようにするため、次のようにしてコルーチンのモジュールをGradleでインストールします。
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
ScopedAppActivityクラスの作成
こちらの公式ドキュメントを参考にプログラムを解説していきます。 kotlinx.coroutines/coroutines-guide-ui.md · GitHub Basics - Kotlin Programming Language
公式ドキュメントにあるようにAppCompatActivityを継承したScopedAppActivityクラスを作ります。
abstract class ScopedAppActivity: AppCompatActivity(), CoroutineScope by MainScope() {
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
コルーチンを使うアクティビティでScopedAppActivityを継承させます。ScopedAppActivityによって、onDestroyでコルーチンのJobキャンセルをし忘れることがなくなり、コルーチンの記述も簡潔に書けるようになります。
class MainActivity : ScopedAppActivity() {
...
}
launchを使う
新しいコルーチンを起動するにはlaunchを使います。引数を指定していないのでブロック内はメインスレッドなことに気をつけましょう。
launch {
// メインスレッドで実行される
var counter = 0
while (true) {
textView.text = "${++counter} $message"
delay(1000) // ブロッキングされない
}
}
また、delayはコルーチンを中断する関数です。プログラム内容を覗くと、次のようなsuspendで定義されてます。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
コルーチンで非同期処理を書く
次に、asyncを使って非同期処理を書いてみます。awaitを使うことで実行結果を待つことができます。UIにアクセスする場合は、withContext(Dispatchers.Main)ブロック内に書きます。 ただし、withContext(Dispatchers.Main)の記述がなくても問題なく動きます。また、asyncブロック内の最後の行が戻り値となります。returnはつけてはいけないことになってますので気をつけましょう。
button.setOnClickListener {
launch {
val deferred = async(Dispatchers.IO) {
Thread.sleep(3000)
"リクエスト No.${++requestCount}"
}
withContext(Dispatchers.Main) {
message = deferred.await()
}
}
}
suspend関数
コルーチンのasyncの非同期処理を自作の関数へ移したい場合は、suspendを使います。suspendを使えば、次のように非同期処理を分離でき、可読性が上がります。
var requestCount = 0
suspend fun showIOData() {
val deferred = async(Dispatchers.IO) {
Thread.sleep(3000)
"リクエスト No.${++requestCount}"
}
withContext(Dispatchers.Main) {
message = deferred.await()
}
}
button.setOnClickListener {
launch {
showIOData()
}
}
asyncを使わずwithContextを使って書く
先ほど紹介したプログラムは、次のようにも書くことも可能です。asyncを使った場合との厳密的な違いはわかっていませんが、suspendとwithContextを使ったこちらのほうが、個人的にはわかりやすくて好きです。
var requestCount = 0
suspend fun fetchIOData():String = withContext(Dispatchers.IO) {
Thread.sleep(3000)
"リクエスト No.${++requestCount}"
}
button.setOnClickListener {
launch {
message = fetchIOData()
}
}
View.onClickを拡張する
こちらのプログラムのように、launchの記述がいちいち面倒です。
button.setOnClickListener {
launch {
message = fetchIOData()
}
}
そんな時は、View.onClickを拡張しちゃいましょう。
fun View.onClick(action: suspend (View) -> Unit) {
val eventActor = GlobalScope.actor<View>(Dispatchers.Main, capacity = Channel.CONFLATED) {
for (event in channel) action(event)
}
setOnClickListener {
eventActor.offer(it)
}
}
すると、次のようにイベントハンドラ内がシンプルになりました。
button.onClick {
message = fetchIOData()
}
詳しくは公式ドキュメントをご覧ください。 kotlinx.coroutines/coroutines-guide-ui.md · GitHub
ここまでのMainActivityのまとめ
これまで紹介したMainActivityのプログラムをまとめてみます。
import android.os.Bundle
import android.util.Log
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
class MainActivity : ScopedAppActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setup()
}
fun setup() {
var message = ""
launch {
// メインスレッドで実行される
var counter = 0
while (true) {
textView.text = "${++counter} $message"
delay(1000) // ブロッキングされない
}
}
var requestCount = 0
suspend fun fetchIOData():String = withContext(Dispatchers.IO) {
Thread.sleep(3000)
"リクエスト No.${++requestCount}"
}
button.onClick {
message = fetchIOData()
}
}
fun View.onClick(action: suspend (View) -> Unit) {
val eventActor = GlobalScope.actor<View>(Dispatchers.Main, capacity = Channel.CONFLATED) {
for (event in channel) action(event)
}
setOnClickListener {
eventActor.offer(it)
}
}
}
suspend内のコルーチンのキャンセル
suspend内のコルーチンのキャンセルする方法をご紹介します。ベストプラクティスかどうかはわかりませんが、次のプログラムのように、CoroutineScopeをメンバ変数に保持してcancelTaskを呼び出することでコルーチンをキャンセルできます。object EchoApiGenerator {
var mTask:CoroutineScope? = null
@Throws(IOException::class)
suspend fun download(): String = withContext(Dispatchers.IO) {
mTask = this
...非同期処理
}
fun cancelTask() {
mTask?.cancel()
}
}
コルーチンがキャンセルされた時に、例外が投げられますのでフックします。
try {
message = CoroutineScope.download()
} catch (e:CancellationException) {
println("コルーチンキャンセル! $e")
}
ただし注意点としまして、OkHttpを使った非同期処理を試してみましたが、OkHttpのキャンセルができませんでした。その場合は、OkHttpのCallを呼び出してキャンセルさせましょう。
▼ こんな記事も書いてます。