搭配生命週期感知元件使用 Kotlin 協同程式

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

應用程式中的每個 ViewModel 都有一個 ViewModelScope。如果 ViewModel 已清除,這個範圍中啟動的所有協同程式都會自動取消。如果有工作需要在 ViewModel 啟用時處理,則協同程式就相當實用。舉例來說,如要計算版面配置的某些資料,須將工作範圍設為 ViewModel,這樣在 ViewModel 清除後,工作就會自動取消,以免消耗資源。

您可以透過 ViewModel 的 viewModelScope 屬性存取 ViewModelCoroutineScope,如以下範例所示:

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

生命週期範圍

系統會為每個 Lifecycle 物件定義 LifecycleScope。銷毀 Lifecycle 時,所有在這範圍內的協同程式工作都會取消。您可以透過 lifecycle.coroutineScopelifecycleOwner.lifecycleScope 屬性存取 LifecycleCoroutineScope

以下範例說明如何使用 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)
        }
    }
}

可重新啟動的生命週期感知協同程式

即使 lifecycleScope 是在 LifecycleDESTROYED 的情況下可自動取消長時間執行的作業,但您可能必須在其他情況下開始執行,當 Lifecycle 處於特定狀態時封鎖程式碼,並在其他狀態時取消。舉例來說,您可能要收集當 LifecycleSTARTED 時的流程,然後該收集為 STOPPED 便取消。只有在 UI 顯示在畫面上時,這個方法才會只處理流量排放,藉此節省資源並避免應用程式當機。

對於這類情況,LifecycleLifecycleOwner 會提供真正停權 repeatOnLifecycle 的 API。以下範例包含的程式碼會在每次相關聯的 Lifecycle 至少處於 STARTED 狀態時執行,並在 LifecycleSTOPPED 時取消:

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 位於特定位置狀態。舉例來說,您必須等待 Lifecycle 至少到 STARTED,才能執行 FragmentTransaction。在這些情況下,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 再次生效,系統就會重新啟動。如果在先前的執行作業中成功完成,則不會重新啟動該程式。請注意,只有在自動取消時才會重新啟動。如果程式因其他原因而取消 (例如擲回 CancellationException),系統不會重新啟動。

您也可以從區塊中發出多個值。每次 emit() 呼叫都會暫停區塊的執行,直到主執行緒中的 LiveData 值已設定為止。

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))
        }
    }
}

每當要發送新值時,您都可以呼叫 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)
                }
            )
        }
    }
}

如需更多與協同程式相關的資訊,請參閱下列連結:

其他資源

如要進一步瞭解如何將協同程式與生命週期感知元件搭配使用,請參閱下列資源。

範例

網誌