Posted at

Kotlinのasync/awaitをカジュアルに使う話

More than 1 year has passed since last update.

Kotlinの1.2.xではまだexperimentalだけど、1.3で正式導入予定のCoroutine、特にasync/awaitはAPI呼び出しなどの非同期な処理に便利そうです。

ただ、AndroidのActivityやFragmentにはライフサイクルがあって、Activityが破棄されたあとにViewをいじったりすると例外が発生してしまうという問題があります。

その辺のことをまじめに考えずに、非同期処理を書いてしまっったら、上記の問題はどうなるのでしょうか?

結論を言ってしまうと、適当に書いても問題なさそうです。


MainActivity.kt

class MainActivity : AppCompatActivity() {

companion object {
const val TAG = "MainActivity"
}

private val button by lazy { findViewById<TextView>(R.id.button) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
Log.d(TAG, "clicked")
launch(UI) {
// coroutineからでないとsuspendedなメソッドを使えない(コンパイル時にエラー)
Log.d(TAG, "clicked - launch")
onPressed()
}
Log.d(TAG, "clicked - finish")
finish()
Log.d(TAG, "clicked - finished")
}
}

override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "destroy")
}

private suspend fun onPressed() {
Log.d(TAG, "pressed")
val v = async {
Thread.sleep(5000)
Log.d(TAG, "pressed - wake up")
System.currentTimeMillis().toString()
}
Log.d(TAG, "pressed - before await")
button.text = v.await()
Log.d(TAG, "pressed - after await")
}
}


こんなActivityを書いて、実行してみます。

onPressed()は、suspend(つまり一旦中断される事がある)で、かつ、UIをいじるメソッドなので、launch(UI)内で使うことになります。

onPressed()内での処理は、確認のために遅延が起こればいいだけなので、5秒スリープしてから、ボタンのキャプションを書き換えるようにしました。

さらに、button.setOnClickListenerでは、onPressed()の終了を待たずに、finish()します。

あと、開発者オプションで、アクティビティを保持しないようにしておけば、Activityが破棄されたあとに、Viewの書き換えが起こるはずです。

そのときのログは、こんな感じになりました。

D/MainActivity: clicked

D/MainActivity: clicked - finish
D/MainActivity: clicked - finished
D/MainActivity: pressed
D/MainActivity: pressed - before await
D/MainActivity: destroy
D/MainActivity: pressed - wake up
D/MainActivity: pressed - after await
D/MainActivity: clicked - launch

onDestroyのあとにViewをいじっても、エラーにならないんですね。

ちょっと気持ち悪い感じがしないでもないですが、async/awaitを使った非同期処理は、Activity等のライフサイクルを考えずに、つかってもよしなにしてくれると思っていいみたいです。