Kotlinによる非同期処理とKotlinコルーチン
前回で、非同期で天気情報を取得して表示するアプリが完成しました。今回は、開発言語をKotlinとした場合の話です。Kotlinには、Kotlin独特の非同期処理の方法があります。そのあたりから概観していきましょう。
JavaコードをKotlinコードに置き換えた場合
Kotlin言語はJVM上で動作する言語ですので、基本的にJava言語とほぼ同様に記述できる言語であり、Javaクラスがそのまま利用できます。ということは、前回までに紹介したExecutorServiceやHandler、Looperといったクラス郡を利用したコードを、そのままKotlinコードに置き換えれば、非同期処理が実現できます。実際、そのようなコーディングを行ったサンプルとして、AsyncKotlinSampleプロジェクトをダウンロードサンプルに含めていますので、参考にしてください。
Kotlinコルーチン
一方、Kotlinには、Java由来の非同期処理コードとは別の記述方法として、Kotlinコルーチンというのがあります。こちらは、Java言語にはないものです。
コルーチンは、Kotlin独自のものではなく、1960年代からある考え方で、様々な言語に採用されています。コルーチンの難しい話は別媒体に譲りますが、簡単にいうと、ひとつの非同期処理ブロック内で、処理を途中で中断して、その中断している間に別の処理を実行することができる仕組みです。もちろん、中断した処理は、その後適切なタイミングで再開させることができます。
例えば、HTTPアクセスを行う処理が記述されたbackgroundTaskRunner()メソッドとJSON解析と画面表示処理を行うpostExecutorRunner()メソッドがあるとします。これらは、それぞれ前回のリスト6で記述したBackgroundTaskクラスのrun()メソッド内の記述、および、リスト8で記述したPostExecutorクラスのrun()メソッド内の記述をKotlinに置き換えたものを想定してください。
これら両メソッド内の処理というのは、それぞれワーカースレッドとUIスレッドで動作しますが、その両方ともが非同期処理である必要があります。しかも、backgroundTaskRunner()の処理が終了した後にpostExecutorRunner()が実行される必要があります。でないと、postExecutorRunner()ではbackgroundTaskRunner()の実行結果であるJSON文字列を受け取ることができず、JSON解析が行えません。
このような場合、backgroundTaskRunner()とpostExecutorRunner()をひとつの非同期処理ブロックとした上で、backgroundTaskRunner()実行中は処理を中断します。そして、backgroundTaskRunner()の処理が終了した時点で処理を再開し、postExecutorRunner()を呼び出します。
この関係を図にしたのが、図1です。
Javaでは、このような処理を実現するために、LooperやHanderを利用して、処理を戻す方法をとりましたが、コルーチンを利用すると、以下のコードのように、もっとシンプルにコードが記述できます。
val result = backgroundTaskRunner(url) postExecutorRunner(result)
コルーチンとコルーチンスコープ
図1では、非同期で処理を行うbackgroundTaskRunner()とpostExecutorRunner()をひとつのブロック(非同期処理ブロック)として記述しています。Kotlinでは、この非同期処理のまとまりのことを、コルーチンといいます。そして、そのコルーチンが生存していられる環境をコルーチンスコープといいます。ということは、コルーチンを利用しようとするならば、まず、コルーチンスコープを用意し、そのコルーチンスコープからコルーチンを起動する手順を取ります。こちらに関しては、具体的なコードを交えて後述します。
AndroidでのKotlinコルーチンの利用
さて、そのようなKotlinコルーチンをAndroidで利用する準備を進めていきましょう。
追加ライブラリが必要
AndroidでKotlinコルーチンを利用するためには、標準のSDKのみでは無理であり、以下の2ライブラリを追加する必要があります(バージョン番号は、原稿執筆時点で最新のものを記述しています)。
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0
上のライブラリがKotlinコルーチン本体です。下のライブラリは、Androidでそれらコルーチンライブラリを効率よく利用するためのライブラリです。これらは、build.gradle(:app)のdependenciesに以下のコードを追加することで可能です。
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
今回のサンプルもスケルトンプロジェクトを利用
あらかじめAndroidプロジェクトを、Kotlin言語を利用する設定で作成し、上記ライブラリの追加を行うのが本筋です。しかし、その後のアプリ作成に必要なコードを全て掲載するには、今回も紙面がたりませんので、前回同様スケルトンプロジェクトを利用することにします。ダウンロードサンプル中のAsyncCoroutineSample-Skeleton.zipが該当します。解凍の上、Android Studioで読み込んで、一度エミュレータなどでアプリを実行してみてください。前回の図3の左側と同様の画面が表示されます。
スケルトンプロジェクトに記述済みのbackgroundTaskRunner()のポイント
画面表示に関しては、前回のアプリと全く同じですが、MainActivity内のコードは、当然ですが全てKotlinに置き換わっています。その中でもいくつかポイントを見ていきましょう。
まず、ワーカースレッドでHTTP通信を行うbackgroundTaskRunner()メソッドはリスト2のようなコードになっています。
@WorkerThread // (1) private fun backgroundTaskRunner(url: String): String { var result = "" val url = URL(url) val con = url.openConnection() as? HttpURLConnection con?.run { // (2) requestMethod = "GET" connect() result = is2String(inputStream) // (3) disconnect() inputStream.close() } return result // (4) }
このbackgroundTaskRunner()は、先述のように、基本的に前回のBackgroundTaskクラスのrun()メソッド内の記述をKotlinコードに置き換えたものです。特筆すべき点は、(1)と(2)です、まず、図1にあるように、backgroundTaskRunner()はワーカースレッドで動作するので、(1)のように@WorkerThreadアノテーションを付与します。こちらは、Javaコードと同様です。(2)については、Kotlinでは、セーフコール演算子「?.」が利用できますので、HttpURLConnectionオブジェクトであるconがnullではない場合だけの処理がブロックとして記述できます。
なお、Javaコード同様に、レスポンスとして取得したデータはInputStreamオブジェクトとなっていますので、それを文字列に変換する必要があります。そのためのis2String()メソッドも、Kotlinコードに置き換えたものをあらかじめスケルトンプロジェクトに記述しています。それを呼び出しているのが、(3)です。
最終的に取得した天気情報JSON文字列を(4)でリターンします。
スケルトンプロジェクトに記述済みのpostExecutorRunner()のポイント
リスト1にあるように、コルーチン内ではこのbackgroundTaskRunner()メソッドの戻り値を変数resultで受け取り、それを引数としてpostExecutorRunner()メソッドを呼び出しています。このpostExecutorRunner()メソッドそのものも、スケルトンプロジェクトにはあらかじめ記述しています。それは、リスト3のコードとなります。
@UiThread private fun postExecutorRunner(result: String) { val rootJSON = JSONObject(result) val cityName = rootJSON.getString("name") val weatherJSONArray = rootJSON.getJSONArray("weather") val weatherJSON = weatherJSONArray.getJSONObject(0) val description = weatherJSON.getString("description") val telop = cityName + "の天気" val desc = "現在は" + description + "です。" val tvWeatherTelop = findViewById<TextView>(R.id.tvWeatherTelop) val tvWeatherDesc = findViewById<TextView>(R.id.tvWeatherDesc) tvWeatherTelop.text = telop tvWeatherDesc.text = desc }
このコードも、基本的に前回のPostExecutorクラスのrun()メソッド内の記述を、そのままKotlinコードに置き換えた内容になっています。@UiThreadアノテーションが付与されていることも含めて、リスト2のbackgroundTaskRunner()よりもJavaコードに対応しているといえます。