飴屋

Kotlin/RunnableからCoroutineへ

もうJetpack Composeと直接関係なくなってしまいますが、Composableな関数に画面設計を置き換えてますと、いろいろ古い書き方が目に付くようになりまして、特に今、いくつものRunnable+コールバックというやり方が目の前に立ちふさがっているのでした。Runnableで非同期処理を行い、コールバックで結果を本流に戻してやるというやり方ですね。これ、今までさんざん書いてきましたが、やりたいことに対して書くことが冗長だなってずっと思ってました。KotlinではCoroutineという仕組みを使って、シンプルに書けるよ、とは聞いていたんですよね。何ならKotlinの本領はCoroutineにある、まで聞きましたよ。(ほんとかな?)これまでRunnableを継承したクラスを作ってやっていたことが、数行で書けるとかなんとか・・・。

class MainViewModel : ViewModel() {
    private val _done = MutableStateFlow(false)
    val done: StateFlow<Boolean> = _done
    fun longProc() {
        viewModelScope.launch(Dispatchers.IO) {
            // 何か時間がかかりそうで、UIスレッドと同期処理させられない処理

            withContext(Dispatchers.Main) {
                _done.value = true
            }
        }
    }
}

まず例によってViewModelの中で非同期処理が終わったかどうかを管理する状態を用意しておきます。一緒にコルーチンの中でやりたいことを書いた関数を書きます。longProcという名前で、処理にどの程度時間がかかるか予測ができないような中身になっています。(心の中で)これをそのまま呼び出すと、UIスレッド(メインスレッド)が長時間ストップしてしまうかもしれず、操作が効かず、ユーザーが不安になってしまう時間ができかねません。そこでviewModelScope.launchです。launchという名前のメソッドはこれまでにも出てきていた気がします。これはコルーチンを起ち上げる的な意味だったのかもしれませんね。viewModelScope.launchは名前の通り、ViewModelのスコープを持ったコルーチンが立ち上がるんでしょうか。どうもViewModelがコルーチンの処理完了を待たずに先に死んだ場合、コルーチンも一緒に死んでくれるそうです。キャンセル処理をしなくてもよくて楽ですね。ということは、別のスコープを持ったコルーチンというのも作れるんじゃないでしょうか?ViewModelと一緒に死なれちゃ困るっていう処理もあるでしょうしね。詳しくは調べませんが、コルーチンの死に際(ライフサイクル)について、いろいろと用意されてる雰囲気だけはザックリ感じました。後は、キャンセルを自ら行うこともちゃんとできるそうです。viewModelScope.launchの返り値がJob型のデータで、このコルーチンを表すものみたいで、そのJobを変数にでも入れておいて、任意のタイミングでjob?.cancel()とでもすればキャンセルできるようです。キャンセルはキャンセルであって一時停止ではなく、処理を再開させたい場合は、どんな処理の進捗状況だったのか、自分でしっかり確認する必要がありそうでした。まぁ、今のところそれは必要なさそうです。あっ、キャンセルについてはもう一つ、要件があって、cancelメソッドを呼んでも直ちにキャンセルされるわけではないようです。cancelメソッドはUIスレッド側から呼ばれると思うのですが、そのJobの実処理はコルーチンの別のスレッドで非同期で動いているのですよね。非同期すなわち、メインのスレッドがいくら「キャンセルしろ!」って叫んでも同期されないから声が届かないわけです。なので、このキャンセル命令が出てないか、確認するタイミングをコルーチンの中で指定してあげる必要があります。例えばコルーチンの中でループを回すようなときは、ループの中にyeild命令を書いておけば、ループ一周する度に「キャンセルされてないよね」って確認するようになるわけです。あー、20年以上前にJavaをいじりだしたときにもyieldを挿入してたなぁ。(回想)あの頃と今とではプロセッサが違うし、並列処理はなかったんだっけ?yeildが「譲渡する」くらいの意味だと思うんですが、スレッド間でどこを走るか決めるのに「譲渡」っていう単語があんまりピンと来なくて、理解に苦しんでいた気がします。

そして、コルーチンの処理が終わったら、今度はコルーチンの中でwithContextを使ってメインのUIスレッドに戻り、処理が終わったことを状態を変更させることで伝えます。ここではブール値をtrueに書き換えてます。これでComposableな関数にも処理の終わりが伝わるわけですね。コルーチンでの計算結果をComposableな関数に伝えたかったら、状態にそれを含めてやればいいですね。

Runnableでやってたことは、これで一通りできる気がしてきました。やはりコルーチン化した方がシンプルに書けて便利な気がしますね。もっと複雑なことをしたいときはまた改めてやり方を考えればいいですかね。

一覧へ