前の記事で、 Androidでのasync-awaitの概要を簡単に説明しました。 今度は、kotlinバージョン1.1の機能についてもう少し詳しく説明します。
async-awaitとは何ですか?
ネットワーククエリやデータベース内のトランザクションなど、時間のかかる操作に直面した場合、バックグラウンドスレッドで起動が行われることを確認する必要があります。 忘れた場合は、タスクが終了する前でもUIスレッドのブロックを取得できます。 また、UIをブロックしている間、ユーザーはアプリケーションと対話できなくなります。
残念ながら、バックグラウンドでタスクを実行すると、すぐに結果を使用できません。 これを行うには、何らかのコールバックが必要です。 結果でコールバックが呼び出されると、その後だけ、たとえば別のネットワーク要求を起動できます。
人々が「 コールバック地獄 」に来る方法の簡単な例:いくつかのネストされたコールバック、再生操作が終了したときにすべてがコールを待っています。
fun retrieveIssues() { githubApi.retrieveUser() { user -> githubApi.repositoriesFor(user) { repositories -> githubApi.issueFor(repositories.first()) { issues -> handler.post { textView.text = "You have issues!" } } } } }
このコードは3つのネットワーク要求を表します。最後に、特定のTextViewを更新するためにメッセージがメインスレッドに送信されます。
async-awaitによる修正
async-awaitを使用すると、このコードを同じ機能を備えたより命令的なスタイルにすることができます。 コールバックを送信する代わりに、「freezing」 awaitメソッドを呼び出すことができます。これにより、結果を同期コードで計算されたように使用できます。
fun retrieveIssues() = asyncUI { val user = await(githubApi.retrieveUser()) val repositories = await(githubApi.repositoriesFor(user)) val issues = await(githubApi.issueFor(repositories.first())) textView.text = "You have issues!" }
このコードはまだ3つのネットワーク要求を行い、メインスレッドでTextViewを更新し、UIをブロックしません!
待って…何?
AsyncAwait-Androidライブラリを使用する場合、いくつかのメソッドを取得します。そのうちの2つはasyncとawaitです。
asyncメソッドを使用すると、 awaitを使用して結果を取得する方法を変更できます。 メソッドに入ると、各行は「フリーズ」ポイントに到達するまで同期して実行されます( awaitメソッドを呼び出します)。 実際、これが非同期のすべてです。コードをバックグラウンドスレッドに移動しないようにできます。
awaitメソッドを使用すると、非同期で処理を実行できます。 パラメータとして「awaitable」を取ります。「awaitable」は何らかの非同期操作です。 awaitが呼び出されると、「awaitable」に登録して、操作が完了したときに通知を受信し、結果をasyncUIメソッドに返します。 「待機可能」が完了すると、メソッドを実行し、そこに結果を渡します。
魔法
それはすべて魔法のように聞こえますが、魔法はありません。 実際、kotlinコンパイラーはコルーチン( async内にあるもの)を状態マシン(状態マシン)に変換します。 各状態はコルーチンからのコードの一部であり、「フリーズ」ポイント(呼び出しを待つ )が状態の終了を示します。 awaitに渡されたコードが完了すると、実行は次の状態に進みます。
前に示したコードの単純なバージョンを検討してください。 作成された状態を確認できます。このため、 awaitの各呼び出しに注意してください。
fun retrieveIssues() = async { println("Retrieving user") val user = await(githubApi.retrieveUser()) println("$user retrieved") val repositories = await(githubApi.repositoriesFor(user)) println("${repositories.size} repositories") }
このコルーチンには3つの状態があります。
- 待機する前の初期状態
- 最初の待機呼び出しの後
- 2回目の待機呼び出し後
このコードは、ステートマシンにコンパイルされます(擬似バイトコード):
class <anonymous_for_state_machine> { // The current state of the machine int label = 0 // Local variables for the coroutine User user = null List<Repository> repositories = null void resume (Object data) { if (label == 0) goto L0 if (label == 1) goto L1 if (label == 2) goto L2 L0: println("Retrieving user") // Prepare for await call label = 1 await(githubApi.retrieveUser(), this) // 'this' is passed as a continuation return L1: user = (User) data println("$user retrieved") label = 2 await(githubApi.repositoriesFor(user), this) return L2: repositories = (List<Repository>) data println("${repositories.size} repositories") label = -1 return } }
ステートマシンに入った後、 ラベル== 0で、コードの最初のブロックが実行されます。 awaitに達すると、 ラベルが更新され、ステートマシンはawaitに渡されたコードの実行に進みます。 その後、実行は再開ポイントから続行されます。
awaitに送信されたタスクが完了した後、ステートマシンの再開(データ)メソッドが呼び出され、次の部分を完了します。 そして、これは最後の状態に達するまで続きます。
例外処理
awaitableがエラーで完了すると、ステートマシンに通知されます。 実際には、 resumeメソッドは追加のThrowableパラメーターを受け入れ、新しい状態が実行されると、このパラメーターのnullがチェックされます 。 パラメーターがnullの場合、 Throwableはスローされます。
したがって、通常どおりtry / catchステートメントを使用できます。
fun foo() = async { try { await(doSomething()) await(doSomethingThatThrows()) } catch(t: Throwable) { t.printStackTrace() } }
マルチスレッド
awaitメソッドは、バックグラウンドスレッドでawaitableが開始することを保証しませんが 、 awaitableの完了に応答するリスナーを登録するだけです。 したがって、 awaitableは実行を実行するスレッドを処理する必要があります。
たとえば、 retrofit.Call <T>をawaitに送信し、 enqueue()メソッドを呼び出して、リスナーを登録します。 Retrofitは、ネットワーク要求がバックグラウンドスレッドで起動されるように注意します。
suspend fun <R> await( call: Call<R>, machine: Continuation<Response<R>> ) { call.enqueue( { response -> machine.resume(response) }, { throwable -> machine.resumeWithException(throwable) } ) }
便宜上、 awaitメソッドの1つのバリアントがあり、関数()-> Rを取り、別のスレッドで実行します。
fun foo() = async<String> { await { "Hello, world!" } }
async、async <T>およびasyncUI
非同期メソッドには3つのバリエーションがあります
- async :何も返しません( Unitやvoidなど )
- async <T> :タイプTの値を返します
- asyncUI :何も返しません
async <T>を使用する場合、タイプTの値を返す必要があります。 非同期<T>メソッド自体がTask <T>型の値を返します。これは、おそらくご想像のとおり、 awaitメソッドに送信できます。
fun foo() = async { val text = await(bar()) println(text) } fun bar() = async<String> { "Hello world!" }
さらに、 asyncUIメソッドは、メインスレッドで継続( await間のコード)が発生することを保証します。 asyncまたはasync <T>を使用する場合、コールバックが呼び出されたのと同じスレッドで継続が発生します。
fun foo() = async { // Runs on calling thread await(someIoTask()) // someIoTask() runs on an io thread // Continues on the io thread } fun bar() = asyncUI { // Runs on main thread await(someIoTask()) // someIoTask() runs on an io thread // Continues on the main thread }
結論として
お気づきかもしれませんが、コルーチンは興味深い機能を提供し、正しく使用すればコードの可読性を改善できます。 現在、kotlinバージョン1.1-M02で使用でき、githubのライブラリを使用して、この記事で説明されているasync-await機能を使用できます。