在 Android 應用中,通常需要從 UI 層收集 Kotlin 數據流,以便在屏幕上顯示數據更新。同時,您也會希望通過收集這些數據流,來避免產生不必要的操作和資源浪費 (包括 CPU 和內存),以及防止在 View 進入后臺時泄露數據。
本文將會帶您學習如何使用 LifecycleOwner.addRepeatingJob
、Lifecycle.repeatOnLifecycle
以及 Flow.flowWithLifecycle
API 來避免資源的浪費;同時也會介紹為什么這些 API 適合作為在 UI 層收集數據流時的默認選擇。
資源浪費
無論數據流生產者的具體實現如何,我們都 推薦 從應用的較底層級暴露 Flow<T> API。不過,您也應該保證數據流收集操作的安全性。
使用一些現存 API (如 CoroutineScope.launch、Flow<T>.launchIn 或 LifecycleCoroutineScope.launchWhenX) 收集基于 channel 或使用帶有緩沖的操作符 (如 buffer、conflate、flowOn 或 shareIn) 的冷流的數據是 不安全的,除非您在 Activity 進入后臺時手動取消啟動了協程的 Job。這些 API 會在內部生產者在后臺發送項目到緩沖區時保持它們的活躍狀態,而這樣一來就浪費了資源。
注意: 冷流 是一種數據流類型,這種數據流會在新的訂閱者收集數據時,按需執行生產者的代碼塊。
例如下面的例子中,使用 callbackFlow 發送位置更新的數據流:
// 基于 Channel 實現的冷流,可以發送位置的更新
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // 在出現異常時關閉 Flow
}
// 在 Flow 收集結束時進行清理操作
awaitClose {
removeLocationUpdates(callback)
}
}
使用任意前述 API 從 UI 層收集此數據流都會導致其持續發送位置信息,即使視圖不再展示數據也不會停止!示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 處于 STARTED 狀態時從數據流收集數據,并在
// 生命周期進入 STOPPED 狀態時 SUSPENDS(掛起)收集操作。
// 在 View 轉為 DESTROYED 狀態時取消數據流的收集操作。
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
// 同樣的問題也存在于:
// - lifecycleScope.launch { /* 在這里從 locationFlow() 收集數據 */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
lifecycleScope.launchWhenStarted
掛起了協程的執行。雖然新的位置信息沒有被處理,但 callbackFlow
生產者仍然會持續發送位置信息。使用 lifecycleScope.launch
或 launchIn
API 會更加危險,因為視圖會持續消費位置信息,即使處于后臺也不會停止!這種情況可能會導致您的應用崩潰。
為了解決這些 API 所帶來的問題,您需要在視圖轉入后臺時手動取消收集操作,以取消 callbackFlow
并避免位置提供者持續發送項目并浪費資源。舉例來說,您可以像下面的例子這樣操作:
class LocationActivity : AppCompatActivity() {
// 位置的協程監聽器
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖。
}
}
}
override fun onStop() {
// 在視圖進入后臺時停止收集數據
locationUpdatesJob?.cancel()
super.onStop()
}
}
這是一個不錯的解決方案,美中不足的是有些冗長。如果這個世界有一個有關 Android 開發者的普遍事實,那一定是我們都不喜歡編寫模版代碼。不必編寫模版代碼的一個最大好處就是——寫的代碼越少,出錯的概率越小!
LifecycleOwner.addRepeatingJob
現在我們境遇相同,并且也知道問題出在哪里,是時候找出一個解決方案了。我們的解決方案需要: 1. 簡單;2. 友好或者說便于記憶與理解;更重要的是 3. 安全!無論數據流的實現細節如何,它都應能夠應對所有用例。
事不宜遲——您應該使用的 API 是 lifecycle-runtime-ktx 庫中所提供的 LifecycleOwner.addRepeatingJob
。請參考下面的代碼:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 處于 STARTED 狀態時從數據流收集數據,并在
// 生命周期進入 STOPPED 狀態時 STOPPED(停止)收集操作。
// 它會在生命周期再次進入 STARTED 狀態時自動開始進行數據收集操作。
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
}
}
addRepeatingJob
接收 Lifecycle.State 作為參數,并用它與傳入的代碼塊一起,在生命周期到達該狀態時,自動創建并啟動新的協程;同時也會在生命周期低于該狀態時取消正在運行的協程。
由于 addRepeatingJob
會在協程不再被需要時自動將其取消,因而可以避免產生取消操作相關的模版代碼。您也許已經猜到,為了避免意外行為,這一 API 需要在 Activity 的 onCreate 或 Fragment 的 onViewCreated
方法中調用。下面是配合 Fragment 使用的示例:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地圖
}
}
}
}
注意: 這些 API 在
lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01
庫或其更新的版本中可用。
使用 repeatOnLifecycle
出于提供更為靈活的 API 以及保存調用中的 CoroutineContext
的目的,我們也提供了 掛起函數 Lifecycle.repeatOnLifecycle
供您使用。repeatOnLifecycle
會掛起調用它的協程,并會在進出目標狀態時重新執行代碼塊,最后在 Lifecycle
進入銷毀狀態時恢復調用它的協程。
如果您需要在重復工作前執行一次配置任務,同時希望任務可以在重復工作開始前保持掛起,該 API 可以幫您實現這樣的操作。示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 單次配置任務
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期進入 STARTED 狀態時開始重復任務,在 STOPED 狀態時停止
// 對 expensiveObject 進行操作
}
// 當協程恢復時,`lifecycle` 處于 DESTROY 狀態。repeatOnLifecycle 會在
// 進入 DESTROYED 狀態前掛起協程的執行
}
}
}
Flow.flowWithLifecycle
當您只需要收集一個數據流時,也可以使用 Flow.flowWithLifecycle
操作符。這一 API 的內部也使用 suspend Lifecycle.repeatOnLifecycle
函數實現,并會在生命周期進入和離開目標狀態時發送項目和取消內部的生產者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地圖
}
.launchIn(lifecycleScope)
}
}
注意:
Flow.flowWithLifecycle
API 的命名以Flow.flowOn(CoroutineContext)
為先例,因為它會在不影響下游數據流的同時修改收集上游數據流的CoroutineContext
。與flowOn
相似的另一點是,Flow.flowWithLifecycle
也加入了緩沖區,以防止消費者無法跟上生產者。這一特點源于其實現中使用的callbackFlow
。
配置內部生產者
即使您使用了這些 API,也要小心那些可能浪費資源的熱流,就算它們沒有被收集亦是如此!雖然針對這些熱流有一些合適的用例,但是仍要多加注意并在必要時進行記錄。另一方面,在一些情況下,即使可能造成資源的浪費,令處于后臺的內部數據流生產者保持活躍狀態也會利于某些用例,如: 您需要即時刷新可用數據,而不是去獲取并暫時展示陳舊數據。您可以根據用例決定生產者是否需要始終處于活躍狀態。
您可以使用 MutableStateFlow
與 MutableSharedFlow
兩個 API 中暴露的 subscriptionCount
字段來控制它們,當該字段值為 0 時,內部的生產者就會停止。默認情況下,只要持有數據流實例的對象還在內存中,它們就會保持生產者的活躍狀態。針對這些 API 也有一些合適的用例,比如使用 StateFlow
將 UiState
從 ViewModel 中暴露給 UI。這么做很合適,因為它意味著 ViewModel 總是需要向 View 提供最新的 UI 狀態。
相似的,也可以為此類操作使用 共享開始策略 配置 Flow.stateIn 與 Flow.shareIn 操作符。WhileSubscribed() 將會在沒有活躍的訂閱者時停止內部的生產者!相應的,無論數據流是 Eagerly (積極) 還是 Lazily (惰性) 的,只要它們使用的 CoroutineScope
還處于活躍狀態,其內部的生產者就會保持活躍。
注意: 本文中所描述的 API 可以很好的作為默認從 UI 收集數據流的方式,并且無論數據流的實現方式如何,都應該使用它們。這些 API 做了它們要做的事: 在 UI 于屏幕中不可見時,停止收集其數據流。至于數據流是否應該始終處于活動狀態,則取決于它的實現。
在 Jetpack Compose 中安全地收集數據流
Flow.collectAsState
函數可以在 Compose 中收集來自 composable 的數據流,并可以將值表示為 State<T>,以便能夠更新 Compose UI。即使 Compose 在宿主 Activity 或 Fragment 處于后臺時不會重組 UI,數據流生產者仍會保持活躍并會造成資源的浪費。Compose 可能會遭遇與 View 系統相同的問題。
在 Compose 中收集數據流時,可以使用 Flow.flowWithLifecycle
操作符,示例如下:
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
// 當前位置,可以拿它做一些操作
}
注意,您 需要記得 生命周期感知型數據流使用 locationFlow
與 lifecycleOwner
作為鍵,以便始終使用同一個數據流,除非其中一個鍵發生改變。
Compose 的副作用 (Side-effect) 便是必須處在 受控環境中,因此,使用 LifecycleOwner.addRepeatingJob
不安全。作為替代,可以使用 LaunchedEffect
來創建跟隨 composable 生命周期的協程。在它的代碼塊中,如果您需要在宿主生命周期處于某個 State 時重新執行一個代碼塊,可以調用掛起函數 Lifecycle.repeatOnLifecycle
。
對比 LiveData
您也許會覺得,這些 API 的表現與 LiveData 很相似——確實是這樣!LiveData 可以感知 Lifecycle,而且它的重啟行為使其十分適合觀察來自 UI 的數據流。同理 LifecycleOwner.addRepeatingJob
、suspend Lifecycle.repeatOnLifecycle
以及 Flow.flowWithLifecycle
等 API 亦是如此。
在純 Kotlin 應用中,使用這些 API 可以十分自然地替代 LiveData 收集數據流。如果您使用這些 API 收集數據流,換成 LiveData (相對于使用協程和 Flow) 不會帶來任何額外的好處。而且由于 Flow 可以從任何 Dispatcher 收集數據,同時也能通過它的 操作符 獲得更多功能,所以 Flow 也更為靈活。相對而言,LiveData 的可用操作符有限,且它總是從 UI 線程觀察數據。
數據綁定對 StateFlow 的支持
另一方面,您會想要使用 LiveData
的原因之一,可能是它受到數據綁定的支持。不過 StateFlow 也一樣!更多有關數據綁定對 StateFlow 的支持信息,請參閱 官方文檔。
在 Android 開發中,請使用 LifecycleOwner.addRepeatingJob
、suspend Lifecycle.repeatOnLifecycle
或Flow.flowWithLifecycle
從 UI 層安全地收集數據流。