Kotlin のコルーチンには、非同期コードを記述できる API が用意されています。Kotlin コルーチンを使用すると、CoroutineScope
を定義して、コルーチンを実行するタイミングを管理できます。非同期操作はそれぞれ、特定のスコープで実行されます。
ライフサイクル対応コンポーネントは、アプリ内の論理スコープ用コルーチンと、LiveData
との相互運用レイヤを万全の体制でサポートしています。このトピックでは、ライフサイクル対応コンポーネントでコルーチンを効果的に使用する方法について説明します。
KTX 依存関係を追加する
このトピックで説明する組み込みのコルーチン スコープは、対応する各コンポーネントの KTX 拡張機能に含まれています。これらのスコープを使用するときは、必ず適切な依存関係を追加してください。
ViewModelScope
には、androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
以降を使用します。LifecycleScope
には、androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
以降を使用します。liveData
には、androidx.lifecycle:lifecycle-livedata-ktx:2.4.0
以降を使用します。
ライフサイクル対応コルーチン スコープ
ライフサイクル対応コンポーネントでは、アプリで使用できる次の組み込みスコープが定義されます。
ViewModelScope
ViewModelScope
は、アプリで ViewModel
ごとに定義されます。このスコープ内で起動されたすべてのコルーチンは、ViewModel
が消去されると自動的にキャンセルされます。ViewModel
がアクティブな場合にのみ行う必要がある作業があるとき、コルーチンが役に立ちます。たとえばレイアウト用のデータを計算している場合、作業を ViewModel
にスコープする必要があるため、ViewModel
が消去されると、作業はリソースの消費を避けるために自動的にキャンセルされます。
ViewModel
の CoroutineScope
にアクセスするには、次の例に示すように ViewModel の viewModelScope
プロパティを使用します。
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
LifecycleScope
は、Lifecycle
オブジェクトごとに定義されます。このスコープ内で起動されたすべてのコルーチンは、Lifecycle
が破棄されたときにキャンセルされます。Lifecycle
の CoroutineScope
には、lifecycle.coroutineScope
プロパティまたは lifecycleOwner.lifecycleScope
プロパティを介してアクセスできます。
以下の例は、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)
}
}
}
再実行可能なライフサイクル対応コルーチン
長時間実行オペレーションは、Lifecycle
が DESTROYED
の場合、lifecycleScope
により適切な方法で自動的にキャンセルされますが、Lifecycle
が特定の状態のときにコードブロックの実行の開始が、また別の状態のときにキャンセルが必要な場合もあります。たとえば、Lifecycle
が STARTED
のときに Flow を収集し、STOPPED
になるとコレクションをキャンセルする必要がある場合もあります。この方法では、UI が画面に表示されるときにのみ Flow 出力を処理することで、リソースを節約し、場合によってはアプリのクラッシュを回避します。
このようなケースのために、Lifecycle
と LifecycleOwner
にはこれを実現する suspend repeatOnLifecycle
API が用意されています。次の例には、関連する Lifecycle
が少なくとも STARTED
状態にあるたびに実行し、Lifecycle
が STOPPED
になるとキャンセルするコードブロックが含まれています。
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
ライフサイクル対応フロー収集
ライフサイクル対応収集を単一のフローで行うだけでよい場合は、Flow.flowWithLifecycle()
メソッドを使用してコードを簡略化できます。
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
ただし、複数のフローで並行してライフサイクル対応収集を行う必要がある場合は、各フローを別々のコルーチンで収集する必要があります。その場合は、repeatOnLifecycle()
を直接使用すると効率的です。
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Because collect is a suspend function, if you want to
// collect multiple flows in parallel, you need to do so in
// different coroutines.
launch {
flow1.collect { /* Process the value. */ }
}
launch {
flow2.collect { /* Process the value. */ }
}
}
}
ライフサイクル対応コルーチンを停止する
長時間実行オペレーションは、CoroutineScope
により適切な方法で自動的にキャンセルされますが、Lifecycle
が特定の状態でない限り、コードブロックの実行の停止が必要な場合もあります。たとえば、FragmentTransaction
を実行するには、Lifecycle
が少なくとも STARTED
になるまで待つ必要があります。このような場合、Lifecycle
では、lifecycle.whenCreated
、lifecycle.whenStarted
、lifecycle.whenResumed
などの追加メソッドを提供します。Lifecycle
が必要最小限の状態にない場合、これらのブロック内で実行されているすべてのコルーチンが停止します。
下記の例には、関連する Lifecycle
が少なくとも STARTED
状態の場合にのみ実行するコードブロックが含まれています。
class MyFragment: Fragment {
init { // Notice that we can safely launch in the constructor of the Fragment.
lifecycleScope.launch {
whenStarted {
// The block inside will run only when Lifecycle is at least STARTED.
// It will start executing when fragment is started and
// can call other suspend methods.
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
when
メソッドのいずれかを介してコルーチンが有効なときに、Lifecycle
が破棄されると、そのコルーチンは自動的にキャンセルされます。下記の例では、Lifecycle
の状態が DESTROYED
になると、finally
ブロックが実行します。
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
LiveData でコルーチンを使用する
LiveData
を使用する場合、値の非同期的な計算が必要になることがあります。たとえば、ユーザーの優先設定を取得して UI に提供する場合などです。このような場合、liveData
ビルダー関数を使用して suspend
関数を呼び出し、結果を LiveData
オブジェクトとして提供できます。
以下の例で、loadUser()
は他の場所で宣言された中断関数です。liveData
ビルダー関数を使用して loadUser()
を非同期で呼び出したら、emit()
を使用して結果を出力します。
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
liveData
要素は、コルーチンと LiveData
の間で構造化された同時実行プリミティブとして機能します。コードブロックの実行は、LiveData
が有効になると開始し、LiveData
が無効になったときに設定可能なタイムアウトが経過すると自動的にキャンセルされます。完了前にキャンセルされた場合は、LiveData
が再びアクティブになると再開されます。以前の実行で正常に完了した場合は再開されません。再開されるのは、自動的にキャンセルされた場合だけです。他の理由(CancellationException
のスローなど)でブロックがキャンセルされた場合は、再開されません。
ブロックから複数の値を出力することもできます。メインスレッドで LiveData
値が設定されるまで、各 emit()
呼び出しによりブロックの実行が停止します。
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)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
コルーチンについて詳しくは、以下のリンク先をご覧ください。
参考情報
ライフサイクル対応コンポーネントでコルーチンを使用する方法について詳しくは、以下のリソースをご覧ください。
サンプル
ブログ
- Android でコルーチンを使用する: アプリケーション パターン
- Android の簡単なコルーチン: viewModelScope
- 連続した 2 つの LiveData 出力をコルーチンでテストする
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- LiveData の概要
- ライフサイクル対応コンポーネントによるライフサイクルへの対応
- ページング データを読み込む、表示する