Android 11 デベロッパー プレビュー 2 が公開されました。ぜひお試しのうえ、フィードバックをお寄せください

アーキテクチャ コンポーネントで Kotlin コルーチンを使用する

Kotlin のコルーチンには、非同期コードを記述できる API が用意されています。Kotlin コルーチンを使用すると、CoroutineScope を定義できます。これは、コルーチンを実行するタイミングを管理するのに役立ちます。非同期操作はそれぞれ、特定のスコープで実行されます。

アーキテクチャ コンポーネントは、アプリ内の論理スコープ用コルーチンと、LiveData との相互運用レイヤを万全の体制でサポートしています。このトピックでは、アーキテクチャ コンポーネントでコルーチンを効果的に使用する方法について説明します。

KTX 依存関係を追加する

このトピックで説明する組み込みのコルーチン スコープは、対応するアーキテクチャ コンポーネントごとに KTX 拡張機能に含まれています。これらのスコープを使用するときは、必ず適切な依存関係を追加してください。

  • ViewModelScope の場合は、androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 以降を使用します。
  • LifecycleScope の場合は、androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 以降を使用します。
  • liveData の場合は、androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 以降を使用します。

ライフサイクル対応コルーチン スコープ

アーキテクチャ コンポーネントでは、アプリで使用できる次の組み込みスコープが定義されます。

ViewModelScope

ViewModelScope は、アプリで ViewModel ごとに定義されます。このスコープで開始されたコルーチンは、ViewModel が消去されるとすべて自動的にキャンセルされます。ViewModel がアクティブな場合にのみ行う必要がある作業があるとき、コルーチンが、ここで役に立ちます。たとえば、レイアウト用のデータを計算している場合、作業は ViewModel に対してスコープする必要があるため、ViewModel が消去されると、その作業はリソースの消費を避けるために自動的にキャンセルされます。

次の例に示すように、ViewModelCoroutineScope には、ViewModel の viewModelScope プロパティを使ってアクセスできます。

class MyViewModel: ViewModel() {
        init {
            viewModelScope.launch {
                // Coroutine that will be canceled when the ViewModel is cleared.
            }
        }
    }
    

LifecycleScope

LifecycleScopeLifecycle オブジェクトごとに定義されます。このスコープで開始されたコルーチンは、Lifecycle が破棄されるとすべてキャンセルされます。LifecycleCoroutineScope プロパティにアクセスするには、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 対応コルーチンを停止する

CoroutineScope により適切な方法で実行に時間のかかる処理が自動的にキャンセルされますが、Lifecycle が特定の状態でないときに、コードブロックの実行を停止したい場合もあります。たとえば、FragmentTransaction を実行するには、Lifecycle が少なくとも STARTED になるまで待つ必要があります。このような場合、Lifecycle では、lifecycle.whenCreatedlifecycle.whenStartedlifecycle.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 が再びアクティブになると再開されます。以前の実行で正常に完了した場合は再開されません。再開されるのは、自動的にキャンセルされた場合だけであることに注意してください。ブロックが他の理由(CancelationException のスローなど)でキャンセルされた場合、ブロックは再開されません

ブロックから複数の値を出力することもできます。メインスレッドで LiveData 値が設定されるまで、各 emit() 呼び出しによってブロックの実行が停止されます。

val user: LiveData<Result> = liveData {
        emit(Result.loading())
        try {
            emit(Result.success(fetchUser()))
        } catch(ioException: Exception) {
            emit(Result.error(ioException))
        }
    }
    

次の例に示すように、liveDataTransformations を組み合わせることもできます。

class MyViewModel: ViewModel() {
        private val userId: LiveData<String> = MutableLiveData()
        val user = userId.switchMap { id ->
            liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
                emit(database.loadUserById(id))
            }
        }
    }
    

LiveData から複数の値を出力するには、新しい値を出力する必要があるときに必ず emitSource() 関数を呼び出します。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)
                    }
                )
            }
        }
    }
    

コルーチンについて詳しくは、以下のリンク先をご覧ください。