這是我參與8月更文挑戰的第2天,活動詳情查看:8月更文挑戰markdown
首先認可這個系列有點標題黨,Jetpack 的 MVVM 自己沒有錯,錯在開發者的某些使用不當。本系列將分享那些 AAC 中常見的錯誤用法,幫助你們打造更健康的應用架構架構
自 StateFlow/ SharedFlow 出現後, 官方開始推薦在 MVVM 中使用 Flow 替換 LiveData。 ( 見文章:從 LiveData 遷移到 Kotlin 數據流 )app
Flow 基於協程實現,具備豐富的操做符,經過這些操做符能夠實現線程切換、處理流式數據,相比 LiveData 功能更增強大。 但惟有一點不足,沒法像 LiveData 那樣感知生命週期。ide
感知生命週期爲 LiveData 至少帶來如下兩個好處:函數
- 避免泄漏:當 lifecycleOwner 進入 DESTROYED 時,會自動刪除 Observer
- 節省資源:當 lifecycleOwner 進入 STARTED 時纔開始接受數據,避免 UI 處於後臺時的無效計算。
Flow 也須要作到上面兩點,才能真正地替代 LiveData。oop
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
又爲咱們提供了 LaunchWhenStarted
和 LaunchWhenResumed
( 下文統稱爲 LaunchWhenX
)lua
LaunchWhenX
會在 lifecycleOwner 進入 X 狀態以前一直等待,又在離開 X 狀態時掛起協程。 lifecycleScope + launchWhenX 的組合終於使 Flow 有了與 LiveData 相媲美的生命週期可感知能力:
- 避免泄露:當 lifecycleOwner 進入 DESTROYED 時, lifecycleScope 結束協程
- 節省資源:當 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 仍然是不夠的,沒法徹底避免資源的浪費
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。
順道提一點,前面舉得地圖SDK的例子是個冷流的例子,對於熱流(StateFlow/SharedFlow)是否有必要使用 repeatOnLifecycle 呢? 我的認爲熱流的使用場景中,像前面例子那樣的狀況會少一些,可是在 StateFlow/SharedFlow 的實現中,須要爲每一個 FlowCollector
分配一些資源,若是 FlowCollector
能即便銷燬也是有利的,同時爲了保持寫法的統一,不管冷流熱流都建議使用 repeatOnLifecycle
當咱們只有一個 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()
}
複製代碼
系列文章