Jetpack MVVM七宗罪 之二:在 launchWhenX 中啓動協程

這是我參與8月更文挑戰的第2天,活動詳情查看:8月更文挑戰markdown

首先認可這個系列有點標題黨,Jetpack 的 MVVM 自己沒有錯,錯在開發者的某些使用不當。本系列將分享那些 AAC 中常見的錯誤用法,幫助你們打造更健康的應用架構架構

Flow vs LiveData

自 StateFlow/ SharedFlow 出現後, 官方開始推薦在 MVVM 中使用 Flow 替換 LiveData。 ( 見文章:從 LiveData 遷移到 Kotlin 數據流 )app

Flow 基於協程實現,具備豐富的操做符,經過這些操做符能夠實現線程切換、處理流式數據,相比 LiveData 功能更增強大。 但惟有一點不足,沒法像 LiveData 那樣感知生命週期。ide

感知生命週期爲 LiveData 至少帶來如下兩個好處:函數

  1. 避免泄漏:當 lifecycleOwner 進入 DESTROYED 時,會自動刪除 Observer
  2. 節省資源:當 lifecycleOwner 進入 STARTED 時纔開始接受數據,避免 UI 處於後臺時的無效計算。

Flow 也須要作到上面兩點,才能真正地替代 LiveData。oop

lifecycleScope

lifecycle-runtime-ktx 庫提供了 lifecycleOwner.lifecycleScope 擴展,能夠在當前 Activity 或 Fragment 銷燬時結束此協程,防止泄露。post

Flow 也是運行在協程中的,lifecycleScope 能夠幫助 Flow 解決內存泄露的問題:ui

lifecycleScope.launch {
    viewMode.stateFlow.collect { 
       updateUI(it)
    }
}
複製代碼

雖然解決了內存泄漏問題, 可是 lifecycleScope.launch 會當即啓動協程,以後一直運行直到協程銷燬,沒法像 LiveData 僅當 UI 處於前臺才執行,對資源的浪費比較大。this

所以,lifecycle-runtime-ktx 又爲咱們提供了 LaunchWhenStartedLaunchWhenResumed ( 下文統稱爲 LaunchWhenXlua

launchWhenX 的利與弊

LaunchWhenX 會在 lifecycleOwner 進入 X 狀態以前一直等待,又在離開 X 狀態時掛起協程。 lifecycleScope + launchWhenX 的組合終於使 Flow 有了與 LiveData 相媲美的生命週期可感知能力:

  1. 避免泄露:當 lifecycleOwner 進入 DESTROYED 時, lifecycleScope 結束協程
  2. 節省資源:當 lifecycleOwner 進入 STARTED/RESUMED 時 launchWhenX 恢復執行,不然掛起。

但對於 launchWhenX 來講, 當 lifecycleOwner 離開 X 狀態時,協程只是掛起協程而非銷燬,若是用這個協程來訂閱 Flow,就意味着雖然 Flow 的收集暫停了,可是上游的處理仍在繼續,資源浪費的問題解決地不夠完全。

資源浪費

舉一個資源浪費的例子,加深理解

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

}
複製代碼

如上,使用 callbackFlow 封裝了一個 GoogleMap 中獲取位置的服務,requestLocationUpdates 實時獲取最新位置,並經過 Flow 返回

class LocationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 進入 STATED 時,collect 開始接收數據
        // 進入 STOPED 時,collect 掛起
        lifecycleScope.launchWhenStarted {
            locationProvider.locationFlow().collect {
                // Update the UI
            } 
        }
    }
}
複製代碼

LocationActivity 進入 STOPED 時, lifecycleScope.launchWhenStarted 掛起,中止接受 Flow 的數據,UI 也隨之中止更新。可是 callbackFlow 中的 requestLocationUpdates 仍然還在持續,形成資源的浪費。

所以,即便在 launchWhenX 中訂閱 Flow 仍然是不夠的,沒法徹底避免資源的浪費

解決辦法:repeatOnLifecycle

lifecycle-runtime-ktx 自 2.4.0-alpha01 起,提供了一個新的協程構造器 lifecyle.repeatOnLifecycle, 它在離開 X 狀態時銷燬協程,再進入 X 狀態時再啓動協程。從其命名上也能夠直觀地認識這一點,即圍繞某生命週期的進出反覆啓動新協程

使用 repeatOnLifecycle 能夠彌補上述 launchWhenX 對協程僅掛起而不銷燬的弊端。所以,正確訂閱 Flow 的寫法應該以下(以在 Fragment 中爲例):

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            viewMode.stateFlow.collect { ... }
        }
    }
}
複製代碼

當 Fragment 處於 STARTED 狀態時會開始收集數據,而且在 RESUMED 狀態時保持收集,最終在 Fragment 進入 STOPPED 狀態時結束收集過程。

須要注意 repeatOnLifecycle 自己是個掛起函數,一旦被調用,將走不到後續代碼,除非 lifecycle 進入 DESTROYED。

冷流 or 熱流

順道提一點,前面舉得地圖SDK的例子是個冷流的例子,對於熱流(StateFlow/SharedFlow)是否有必要使用 repeatOnLifecycle 呢? 我的認爲熱流的使用場景中,像前面例子那樣的狀況會少一些,可是在 StateFlow/SharedFlow 的實現中,須要爲每一個 FlowCollector 分配一些資源,若是 FlowCollector 能即便銷燬也是有利的,同時爲了保持寫法的統一,不管冷流熱流都建議使用 repeatOnLifecycle

最後:Flow.flowWithLifecycle

當咱們只有一個 Flow 須要收集時,可使用 flowWithLifecycle 這樣一個 Flow 操做符的形式來簡化代碼

lifecycleScope.launch {
     viewMode.stateFlow
          .flowWithLifecycle(this, Lifecycle.State.STARTED)
          .collect { ... }
 }
複製代碼

固然,其本質仍是對 repeatOnLifecycle 的封裝:

public fun <T> Flow<T>.flowWithLifecycle( lifecycle: Lifecycle, minActiveState: Lifecycle.State = Lifecycle.State.STARTED ): Flow<T> = callbackFlow {
    lifecycle.repeatOnLifecycle(minActiveState) {
        this@flowWithLifecycle.collect {
            send(it)
        }
    }
    close()
}
複製代碼

系列文章

Jetpack MVVM七宗罪之一: 拿 Fragment 當 LifecycleOwner

相關文章
相關標籤/搜索