コルーチンとは、Android で使用できる並行実行のデザイン パターンです。これを使用すると、非同期実行するコードを簡略化できます。コルーチン は、バージョン 1.3 で Kotlin に追加され、確立された コンセプトを学習できます。
Android では、メインスレッドをブロックしてアプリの応答を止める可能性のある長時間実行タスクの管理に役立ちます。コルーチンを使用するプロのデベロッパーの 50% 以上が、生産性が向上したと報告しました。このトピックでは、Kotlin コルーチンを使用してこれらの問題に対処する方法を説明し、より簡潔で無駄のないアプリコードを記述できるようにします。
機能
コルーチンは、Android での非同期プログラミングに推奨するソリューションです。主な機能は次のとおりです。
- 軽量 : 中断 がサポートされているため、1 つのスレッドで多数のコルーチンを実行できます。これにより、コルーチンを実行しているスレッドがブロックされません。中断により、多数の同時実行処理をサポートしつつ、ブロックさせる場合よりメモリを節約できます。
- メモリリークが少ない : 構造化された同時実行 スコープ内でオペレーションを実行できます
- 組み込みの解約サポート : 解約 実行中のコルーチン階層を通じて自動的に伝播されます。
- Jetpack の統合 : Jetpack ライブラリの多くには、コルーチンを全面的にサポートする拡張機能 が用意されています。一部のライブラリでは、構造化された同時実行に使用できる独自のコルーチン スコープ も用意されています。
例の概要
このトピックのサンプルは、アプリ アーキテクチャ ガイド をベースにしており、ネットワーク リクエストを行って、結果をメインスレッドに返し、それをアプリでユーザーに表示するものになっています。
具体的には、
ViewModel
アーキテクチャ コンポーネントがメインスレッドのリポジトリ レイヤを呼び出してネットワーク リクエストをトリガーします。このガイドでは、さまざまなソリューションについて説明します。 コルーチンを使用してメインスレッドのブロックを解除します。
ViewModel
には、コルーチンを直接扱う KTX 拡張機能である
lifecycle-viewmodel-ktx
ライブラリ
が用意されており、このガイドでも使用されています。
依存関係情報
Android プロジェクトでコルーチンを使用するには、アプリの build.gradle
ファイルに次の依存関係を追加します。
Groovy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
バックグラウンド スレッドでの実行
メインスレッドでネットワーク リクエストを行うと、レスポンスを受信するまでの間、待機したり、ブロック
されたりすることになります。スレッドがブロックされるため、OS は onDraw()
を呼び出せず、アプリがフリーズして、アプリケーション応答なし(ANR)のダイアログが表示される可能性があります。ユーザー エクスペリエンスを向上させるために、この処理をバックグラウンド スレッドで実行するようにしましょう。
まず、Repository
クラスを見て、ネットワーク リクエストがどのように行われているかを確認します。
sealed
class
Result<out
R>
{
data
class
Success<out
T>
(
val
data
:
T
)
:
Result<T>
()
data
class
Error
(
val
exception
:
Exception
)
:
Result<Nothing>
()
}
class
LoginRepository
(
private
val
responseParser
:
LoginResponseParser
)
{
private
const
val
loginUrl
=
" https
:
//example.com/login
"
// Function that makes the network request, blocking the current thread
fun
makeLoginRequest
(
jsonBody
:
String
):
Result<LoginResponse>
{
val
url
=
URL
(
loginUrl
)
(
url
.
openConnection
()
as?
HttpURLConnection
)
?.
run
{
requestMethod
=
" POST
"
setRequestProperty
(
" Content
-
Type
" ,
" application
/
json
;
utf
-
8
" )
setRequestProperty
(
" Accept
" ,
" application
/
json
" )
doOutput
=
true
outputStream
.
write
(
jsonBody
.
toByteArray
())
return
Result
.
Success
(
responseParser
.
parse
(
inputStream
))
}
return
Result
.
Error
(
Exception
(
" Cannot
open
HttpURLConnection
" ))
}
}
makeLoginRequest
は同期しており、呼び出し元のスレッドをブロックします。ネットワーク リクエストのレスポンスをモデル化するために、独自の Result
クラスが用意されています。
ViewModel
は、ボタンなどがクリックされたときにネットワーク リクエストをトリガーします。
class
LoginViewModel
(
private
val
loginRepository
:
LoginRepository
):
ViewModel
()
{
fun
login
(
username
:
String
,
token
:
String
)
{
val
jsonBody
=
" {
username
:
\"$
username
\
" ,
token
:
\"$
token
\
" }
"
loginRepository
.
makeLoginRequest
(
jsonBody
)
}
}
前のコードでは、ネットワーク リクエストを行うときに、LoginViewModel
が UI スレッドをブロックしています。この実行をメインスレッドから移動させるには、新しいコルーチンを作成して、I/O スレッドでネットワーク リクエストを実行するのが最も簡単です。
class
LoginViewModel
(
private
val
loginRepository
:
LoginRepository
):
ViewModel
()
{
fun
login
(
username
:
String
,
token
:
String
)
{
// Create a new coroutine to move the execution off the UI thread
viewModelScope
.
launch
(
Dispatchers
.
IO
)
{
val
jsonBody
=
" {
username
:
\"$
username
\
" ,
token
:
\"$
token
\
" }
"
loginRepository
.
makeLoginRequest
(
jsonBody
)
}
}
}
login
関数のコルーチン コードを詳しく見てみましょう。
-
viewModelScope
は定義済みのCoroutineScope
で、ViewModel
KTX 拡張機能に含まれています。すべてのコルーチンはスコープ内で実行する必要があります。CoroutineScope
により、1 つ以上の関連するコルーチンを管理します。 -
launch
は、コルーチンを作成し、この関数自体の実行内容を対応するディスパッチャーにディスパッチする関数です。 -
Dispatchers.IO
は、このコルーチンが I/O 処理用に予約されたスレッドで実行されることを示します。
login
関数は次のように実行されます。
-
アプリが、メインスレッドで
View
レイヤからlogin
関数を呼び出します。 -
launch
が新しいコルーチンを作成し、I/O 処理用に予約されたスレッドで独自にネットワーク リクエストが行われます。 -
このコルーチンの実行中も、
login
関数は実行を継続し、場合によってはネットワーク リクエストが完了する前に戻ります。簡単にするため、ここではネットワーク レスポンスを無視します。
このコルーチンは viewModelScope
で開始されるため、ViewModel
のスコープで実行されます。ユーザーが画面から移動して ViewModel
が破棄された場合、viewModelScope
は自動的にキャンセルされ、実行中のすべてのコルーチンもキャンセルされます。
前のサンプルには、makeLoginRequest
を呼び出すときに、実行を明示的にメインスレッドから移動させる必要があるという問題があります。Repository
を変更して、この問題を解決する方法を見てみましょう。
コルーチンでメインセーフティにする
ある関数がメインスレッドでの UI の更新をブロックしないとき、その関数はメインセーフティ
であると言います。メインスレッドから makeLoginRequest
関数を呼び出すと UI がブロックされるため、makeLoginRequest
はメインセーフティではありません。コルーチンの実行を別のスレッドに移動するために、次のようにコルーチン ライブラリの withContext()
関数を使用します。
class
LoginRepository
(...)
{
...
suspend
fun
makeLoginRequest
(
jsonBody
:
String
):
Result<LoginResponse>
{
// Move the execution of the coroutine to the I/O dispatcher
return
withContext
(
Dispatchers
.
IO
)
{
// Blocking network request code
}
}
}
withContext(Dispatchers.IO)
は、コルーチンの実行を I/O スレッドに移動し、呼び出し元の関数をメインセーフティにして、必要なときに UI が更新されるようにします。
makeLoginRequest
には suspend
キーワードも付いています。このキーワードは、Kotlin 独自のもので、関数がコルーチン内から呼び出されるようにするものです。
次のサンプルでは、LoginViewModel
内でコルーチンが作成されます。makeLoginRequest
の実行がメインスレッドの外に移り、login
関数のコルーチンがメインスレッドで実行されるようになります。
class
LoginViewModel
(
private
val
loginRepository
:
LoginRepository
):
ViewModel
()
{
fun
login
(
username
:
String
,
token
:
String
)
{
// Create a new coroutine on the UI thread
viewModelScope
.
launch
{
val
jsonBody
=
" {
username
:
\"$
username
\
" ,
token
:
\"$
token
\
" }
"
// Make the network call and suspend execution until it finishes
val
result
=
loginRepository
.
makeLoginRequest
(
jsonBody
)
// Display result of the network request to the user
when
(
result
)
{
is
Result
.
Success<LoginResponse>
-
>
// Happy path
else
-
>
// Show error in UI
}
}
}
}
makeLoginRequest
は suspend
関数であり、すべての suspend
関数はコルーチンで実行する必要があるため、ここでは引き続きコルーチンが必要です。
このコードには、前の login
のサンプルとは異なる点があります。
-
launch
にDispatchers.IO
パラメータがない。launch
にDispatcher
を渡さない場合、viewModelScope
から起動されたコルーチンがすべてメインスレッドで実行されます。 - ネットワーク リクエストの結果が処理され、成功または失敗の UI が表示される。
login 関数は次のように実行されます。
-
アプリが、メインスレッドで
View
レイヤからlogin()
関数を呼び出します。 -
launch
がメインスレッドで新しいコルーチンを作成し、そのコルーチンの実行が開始されます。 -
コルーチン内では、
loginRepository.makeLoginRequest()
を呼び出すと、makeLoginRequest()
内のwithContext
ブロックの実行が終了するまでコルーチンの実行を中断 するようになりました。 -
withContext
ブロックが終了すると、login()
のコルーチンはメインスレッド で実行を再開し、ネットワーク リクエストの結果を返します。
例外の処理
Repository
レイヤがスローする例外を処理するには、Kotlin の例外に対する組み込みサポート
を使用します。次のサンプルでは、try-catch
ブロックを使用しています。
class
LoginViewModel
(
private
val
loginRepository
:
LoginRepository
):
ViewModel
()
{
fun
login
(
username
:
String
,
token
:
String
)
{
viewModelScope
.
launch
{
val
jsonBody
=
" {
username
:
\"$
username
\
" ,
token
:
\"$
token
\
" }
"
val
result
=
try
{
loginRepository
.
makeLoginRequest
(
jsonBody
)
}
catch
(
e
:
Exception
)
{
Result
.
Error
(
Exception
(
" Network
request
failed
" ))
}
when
(
result
)
{
is
Result
.
Success<LoginResponse>
-
>
// Happy path
else
-
>
// Show error in UI
}
}
}
}
このサンプルでは、makeLoginRequest()
の呼び出しでスローされた予期しない例外は、UI でエラーとして処理されます。
コルーチンに関する参考情報
Android でのコルーチンの詳細については、Kotlin コルーチンでアプリのパフォーマンスを改善する をご覧ください。
コルーチンに関するその他の参考情報については、次のリンクをご覧ください。