Kotlinのコルーチン(Coroutine)は、非同期処理を行う時に使いますが、
ViewModel・LiveDataで便利に使えるコルーチンスコープというものの説明や、
Roomでコルーチンを使う場合の説明を見つけたりしたので、それについてメモしてみたいと思いますー。
もくじ
元の情報は、公式から。
今回は、こちらの公式サイトを見ながら内容を確認してみました。
Use Kotlin coroutines with Architecture components | Android Developers
わかりやすく書かれているのですが、英語なので後から確認しやすいように、必要そうなところだけ自分なりにメモしてみたいと思いますー。
翻訳したものをメモしただけっぽくなってしまいましたが、必要な箇所だけ参考にしていただければと思います。
ライフサイクルに対応した、コルーチンスコープ
<1. ViewModelScopeというもの>
ViewModelScopeというものがあり、それを使うと、
ViewModelがクリアされると、そこで起動されたコルーチンは、自動的にキャンセルされるようになるらしいです。
そのため、ViewModelがアクティブな時だけに行いたい処理があるときに、便利に使えるようです。
ViewModelScopeを使う時は、こんな風に使うみたいです。
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
//ViewModelがクリアされるとキャンセルされるコルーチン。
}
}
}
そして、これを使うためには、
androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 か、それ以降のバージョン
を使う必要があるようです。
(使うためには、build.gradle(Module: app)に追加するなどして設定します)
<2. LifecycleScopeというもの>
次は、LifecycleScope。
これは、ライフサイクルオブジェクトごとに定義され、
対応するライフサイクルが破棄(destroy)されると、コルーチンもキャンセルされるようになるようです。
これは、ActivityやFragmentなどで使うようです。
lifecycle.coroutineScope または lifecycleOwner.lifecycleScopeプロパティを使用すると、
ライフサイクルのCoroutineScopeが使えるようになるみたいです。
lifecycleOwner.lifecycleScopeを使ったサンプルコードも載っていました。
このサンプルは、事前に計算したテキストを、非同期で作成するようなものになっています。
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
そして、これを使うためには、
androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 か、それ以降のバージョン
を使う必要があるようです。
(使うためには、build.gradle(Module: app)に追加するなどして設定します)
ライフサイクル対応コルーチンを、必要な時だけ使う方法。
ViewModelScopeも、LifecycleScopeも、便利に使えそうですが、
例えば、FragmentTransactionを実行するには、
ライフサイクルが少なくともSTARTEDになるまで待つ必要があったりするため、
ライフサイクルが最低限必要な状態にない場合には、
これらのブロック内で実行されているコルーチンを中断するようにできるようです。
それを実現するための追加メソッドには、こんな種類があるようです。
・lifecycle.whenCreated
・lifecycle.whenStarted
・lifecycle.whenResumed
そして、サンプルコードがこちら。
関連するライフサイクルが、少なくともSTARTED状態にあるときにのみ実行されるようです。
class MyFragment: Fragment {
init { //フラグメントのコンストラクタで安全に起動できることに注意してください。
lifecycleScope.launch {
whenStarted {
// 内部のブロックは、Lifecycleが少なくともSTARTEDされているときにのみ実行されます。
// フラグメントが開始されると実行を開始し、他の中断メソッドを呼び出すことができます。
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
//checkUserAccessが戻ったとき、ライフサイクルが「少なくとも」STARTEDではない場合、次の行は自動的に中断されます。
//ライフサイクルが少なくともSTARTEDでない限り、コードは実行されません。
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
//この行は、上記のwhenStartedブロックが完了した後にのみ実行されます。
}
}
}
コルーチンがwhenメソッドの1つを介してアクティブになっている間に、
ライフサイクルが破棄(destroy)されると、
コルーチンは自動的にキャンセルされます。
次のサンプルでは、ライフサイクルが破棄されると、
finallyが実行されるようです。
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
//いくつかの中断関数を呼び出します。
} finally {
//この行は、Lifecycleが破棄された後に実行される可能性があります。
if (lifecycle.state >= STARTED) {
//ここでは確認したので、Fragmentトランザクションを実行しても安全です。
}
}
}
}
}
注意点として、
情報がライフサイクルの範囲内で有効な場合(たとえば、事前に計算済みのテキスト)にのみ使用するようにしてください、という事でした。
アクティビティが再開(restart)しても、コルーチンは再開されないことに注意してください。
LiveDataでコルーチンを使う場合
まず、ここから説明するLiveData用の機能を使うためには、
androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 か、それ以降のバージョン
を使う必要があるようです。
(使うためには、build.gradle(Module: app)に追加するなどして設定します)
さて、LiveDataを使用するときは、値を非同期に計算したりすると思います。
例えば、ユーザーの設定を取得して、UIに設定する場合。
こんな時は、
liveDataビルダー関数を使用して中断関数(suspend function)を呼び出して、
結果をLiveDataオブジェクトとして返す事ができるようです。
これに関するサンプルも載っていました。
このサンプル内にあるloadUser()は、他の場所で宣言された中断関数です。
liveDataビルダー関数を使用して、loadUser()を非同期的に呼び出して、
次にemit()を使用して結果を発行するような流れになっています。
val user: LiveData<User> = liveData {
val data = database.loadUser() //loadUserは中断関数。
emit(data)
}
LiveDataがアクティブになると実行を開始して、
LiveDataがアクティブじゃなくなると(設定可能なタイムアウト後に)自動的にキャンセルされます。
・完了前にキャンセルされた場合は、
LiveDataが再度アクティブになると再開します。
・前回の実行時に正常に完了した場合は、再起動しません。
(自動的にキャンセルされた場合にのみ再起動されます)
また、複数の値を発行する事もできて、
各emit()は、LiveDataの値がメインスレッドで設定されるまで、
ブロックの実行を中断してくれるみたいです。
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser())
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
次のサンプルのように、liveDataをTransformationsと組み合わせることもできるみたいです。
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
新しい値を発行したいときは、
いつでもemitSource()関数を呼び出すことによって、LiveDataから複数の値を発行できるようです。
emit()またはemitSource()を呼び出すたびに、
前に追加されたソースが削除されることに注意してください、ということでした。
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// 更新されたユーザを `loading`としてディスパッチするのを避けるため、前の発行を止めます。
disposable.dispose()
//データベースを更新します。
userDao.insert(user)
//Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
//`emit`を呼び出すと、前のものが自動的に破棄されるため、更新された値が得られないので、ここで破棄する必要はありません。
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
コルーチン関連の詳細については、以下のリンクを参照してください、という事でしたー。
Improve app performance with Kotlin coroutines
(Kotlinコルーチンでアプリのパフォーマンスを向上させる)
Coroutines overview
(コルーチンの概要)
Threading in CoroutineWorker
(CoroutineWorkerでのスレッド)