コルーチンの作成方法
アプリの骨格が確認できたところで、実際にコルーチンに関するコードを記述していきましょう。
backgroundTaskRunner()とpostExecutorRunner()の記述先
前節で確認したスケルトンプロジェクトに記述してあるbackgroundTaskRunner()とpostExecutorRunner()を順に呼び出すコード、つまり、リスト1のコードは、都市リストがタップされたタイミングで実行される必要があります。そのタイミングで実行されるメソッドは、いうまでもなく、ListItemClickListenerクラス内のonItemClick()メソッドです。スケルトンプロジェクトでは、そのメソッド内部に、Javaコード同様にあらかじめ以下の1行が記述されています。
asyncExecute(url)
そして、このメソッド本体は、スケルトンプロジェクトでは、以下のように何も記述していません。
@UiThread private fun asyncExecute(url: String) { }
ということは、ここに、リスト1の2行を記述して、以下のようなコードにしたとします。
@UiThread private fun asyncExecute(url: String) { val result = backgroundTaskRunner(url) postExecutorRunner(result) }
このコードは一見正しいように思えますが、コンパイルエラーとなります。これを正していきましょう。
ライフサイクルと一致したコルーチンスコープ
まず、図1にあるように、リスト1の2行は、ひとつのコルーチンとする必要があります。そして、コルーチンを起動するには、コルーチンスコープが必要です。コルーチンスコープは、CoroutineScopeインターフェースを実装して独自に作ることもできますが、Androidでコルーチンを利用する場合は、ViewModelに最適化されたスコープであるviewModelScopeと、アクティビティなどのライフサイクルに最適化されたスコープであるlifecycleScopeが拡張プロパティとして用意されています。ここまでのコードは全てアクティビティに記述していますので、ここでは、lifecycleScopeを利用することにします。
どのスコープを利用したとしても、コルーチンを起動するには、launch()メソッドを利用し、ラムダ式としてブロック内に各処理メソッドの呼び出しコードを記述します。結果、リスト4のコードとなります。
@UiThread private fun asyncExecute(url: String) { lifecycleScope.launch { val result = backgroundTaskRunner(url) // (1) postExecutorRunner(result) // (2) } }
処理を中断させるにはsuspendを記述する
次に、図1にあるように、(1)のbackgroundTaskRunner()メソッドの実行中はコルーチン内の処理を中断し、backgroundTaskRunner()の処理が終了してから、(2)のpostExecutorRunner()メソッドの呼び出し処理を再開する必要があります。このように、コルーチン内で該当メソッドの処理中に他の処理を中断させる場合、そのメソッドにsuspendを記述する必要があります。つまり、backgroundTaskRunner()メソッドのシグネチャはリスト5のようになります。
@WorkerThread private suspend fun backgroundTaskRunner(url: String): String { :
メソッド内の処理スレッドを分けるwithContext()関数
これで一見問題ないように思えます。しかし、もう一度図1を見てください。backgroundTaskRunner()メソッドは、ワーカースレッドで行われる処理です。それに対して、asyncExecute()も、その中のlifecycleScope.launch()も、postExecutorRunner()もUIスレッドで動作します。ところが、リスト2、および、リスト5のコードのみでは、backgroundTaskRunner()メソッドは同じくUIスレッドで動作するようになってしまいます。そこで、スレッドを分離するコードを追記します。Kotlinコルーチンでは、スレッドを分離する便利な関数としてwithContext()というのがあるので、これを利用します。すると、リスト6のようなコードになります。
@WorkerThread private suspend fun backgroundTaskRunner(url: String): String { val returnVal = withContext(Dispatchers.IO) { // (1) var result = "" // (2) val url = URL(url) val con = url.openConnection() as? HttpURLConnection con?.run { requestMethod = "GET" connect() result = is2String(inputStream) disconnect() inputStream.close() } result // (3) } return returnVal // (4) }
withContext()関数は、引数にどのようなスレッドに分離するのかをDispatchersクラスの定数として記述します。メインスレッドはDispatchers.Mainで、ワーカースレッドはリスト6の(1)にあるようにDispatchers.IOです。
その続きのラムダ式として、ワーカースレッドで実行した処理を記述します。リスト6では、(2)と(3)に挟まれた行が該当し、これは、まさに、リスト2のあらかじめスケルトンプロジェクトとして記述されていたbackgroundTaskRunner()メソッド内の処理そのものです。ただし、Kotlinの文法として、ラムダ式内の戻り値に関しては、(3)のように、returnを記述しないことになっています。
結果、(3)のresultがwithContext()関数の戻り値としてリターンされ、それを、いったん変数returnValに格納し、(4)でリターンすることで、backgroundTaskRunner()メソッドの戻り値としています。
なお、ここでは可読性を重視し、変数returnValを用意していますが、より省略した形としては、以下のようにできます。
private suspend fun backgroundTaskRunner(url: String): String { return withContext(Dispatchers.IO) { : result } }
さらに、省略して、以下のようにもできます。
private suspend fun backgroundTaskRunner(url: String): String = withContext(Dispatchers.IO) { : result }
これで、一通り正しいコードができました。前回同様、あらかじめMainActivity中に記述された定数APP_IDに自身のAPIキー文字列をコピー&ペーストした上で、動作確認してください。
backgroundTaskRunner()のsuspendの真の意味
ここで、backgroundTaskRunner()メソッドのシグネチャとして記述したsuspendについて少し補足しておきましょう。
実は、backgroundTaskRunner()がワーカースレッドで動作する必要がないならば、すなわち、postExecutorRunner()と同じスレッドで動作するならば、suspendは不要です。第1回で解説した同期処理の話を思い出してください。たとえbackgroundTaskRunner()とpostExecutorRunner()がひとつのコルーチンだったとしても、それが非同期になるのは、コルーチンの内外に関してだけです。リスト4では、asyncExecute()内は、launch()以外の処理が書かれていませんのでわかりにくいコードですが、例えば、以下のコードのような場合は、launch()内の処理の終了を待たずにshowData()メソッドは実行されてしまいます。
private fun asyncExecute(url: String) { lifecycleScope.launch { : } showData() }
一方、コルーチン内の処理に目を向けた場合、それらの処理がひとつのスレッドで行われる限り、それは同期処理となります。必然的に、backgroundTaskRunner()の処理終了を待ってからpostExecutorRunner()が実行されます。ここに、withContext()を使ってbackgroundTaskRunner()のスレッドを分離する必要性が出てきます。逆に、スレッドを分離すると、今度は、backgroundTaskRunner()の処理終了を待たずにpostExecutorRunner()が実行されてしまいます。この帯に短し襷に長しのような状態を解決するのがsuspendキーワードなのです。suspendキーワードが付与されたメソッドや関数は、元のスレッドの処理を中断させることができます。
そして、withContext()は内部的にあらかじめsuspendキーワードが付与された関数なのです。そのsuspendキーワードが付与されたメソッドや関数を内部的に呼び出す限り、そのメソッドにもsuspendを付与する必要があります。これが、backgroundTaskRunner()にsuspendを付与する理由です。
Kotlinコルーチンのコードパターン
ここまでの内容を踏まえて、Kotlinコルーチンでの非同期処理コードパターンをまとめると、以下のようになります。
@UiThread private fun asyncExecute() { lifecycleScope.launch { backgroundTaskRunner() postExecutorRunner() } } @WorkerThread private suspend fun backgroundTaskRunner() { withContext(Dispatchers.IO) { バックグラウンド処理 } } @UiThread private fun postExecutorRunner() { バックグラウンド処理後UIスレッドで行う処理 }
まとめ
以上、全3回にわたって、Androidの非同期処理を扱ってきました。特に、AsyncTaskが非推奨となった現状に合わせてのコードパターンを紹介してきました。ここまで紹介してきた内容が、今後のAndroid開発の役に立てれば、これほど嬉しいことはありません。