從LiveData遷移到Kotlin Flow

兩三行代碼IP屬地: 浙江
3字數 2,751

響應式的框架

RxJava:過于復雜、學習成本高

LiveData:針對Android定制、使用簡單

針對Java開發者,初學者、簡單場景可以考慮使用LiveData。除此以外,可以考慮使用Kotlin Flows。但是Kotlin Flows現在依然有陡峭的學習曲線,但它是Kotlin語言的一部分,由Jetbrains提供支持;另外即將到來的Jetpack Compose 非常適合響應式模式。

Flow:簡單的事情更難,復雜的事情更容易

LiveData擅長于暴露最近獲取的數據,并且能夠結合Android的生命周期。后來我們了解到它也可以啟動協程并創建復雜的轉換,但這有點復雜。

現在讓我們看看一些 LiveData 模式和它們的 Flow 等價寫法:

1、使用可變數據持有者公開一次性操作的結果

這是經典模式,您可以使用協程的結果來改變狀態持有者:

使用可變數據持有者 (LiveData) 公開一次性操作的結果
<!-- Copyright 2020 Google LLC. 
   SPDX-License-Identifier: Apache-2.0 -->

class MyViewModel {
    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)
    val myUiState: LiveData<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

我們可以使用StateFlow來達到相同的效果:

使用可變數據持有者 (StateFlow) 公開一次性操作的結果
class MyViewModel {
    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)
    val myUiState: StateFlow<Result<UiState>> = _myUiState

    // Load data from a suspend fun and mutate state
    init {
        viewModelScope.launch { 
            val result = ...
            _myUiState.value = result
        }
    }
}

StateFlow 是一種特殊的 SharedFlow(它是一種特殊類型的 Flow),最接近 LiveData:

  • 總是有值
  • 只有一個值
  • 支持多個訂閱者
  • 總是重播訂閱的最新值,與活躍觀察者的數量無關

向視圖公開 UI 狀態時,請使用 StateFlow。 它是一個安全高效的觀察者,旨在保持 UI 狀態。

2、公開一次性操作的結果

這與前面的代碼片段等效,公開了沒有可變后備屬性的協程調用的結果。

對于 LiveData,我們為此使用了 liveData 協程構建器:


公開一次性操作的結果 (LiveData)
class MyViewModel(...) : ViewModel() {
    val result: LiveData<Result<UiState>> = liveData {
        emit(Result.Loading)
        emit(repository.fetchItem())
    }
}

由于狀態持有者總是有一個值,因此最好將我們的 UI 狀態包裝在某種支持加載、成功和錯誤等狀態的 Result 類中。

Flow 等價寫法涉及更多,因為您必須進行一些配置:

公開一次性操作的結果 (StateFlow)
class MyViewModel(...) : ViewModel() {
    val result: StateFlow<Result<UiState>> = flow {
        emit(repository.fetchItem())
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), // Or Lazily because it's a one-shot
        initialValue = Result.Loading
    )
}

stateIn 是將 Flow 轉換為 StateFlow 的 Flow 運算符。 現在讓我們相信這些參數,因為我們稍后需要更多的復雜性來正確解釋它。

3、帶參數的一次性數據加載

假設您想加載一些依賴于用戶 ID 的數據,并且您從 AuthManager 的公開的flow獲取此信息:


帶參數的一次性數據加載 (LiveData)

使用 LiveData,您將執行類似以下操作:

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id }.asLiveData()

    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->
        liveData { emit(repository.fetchItem(newUserId)) }
    }
}

switchMap 是一個轉換,它的主體被執行,并且當 userId 改變時訂閱結果。

如果 userId 沒有理由成為 LiveData,那么更好的替代方法是將流與 Flow 結合起來,最后將公開的結果轉換為 LiveData。

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->
       repository.fetchItem(newUserId)
    }.asLiveData()
}

使用 Flows 執行此操作看起來非常相似:


帶參數的一次性數據加載(StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }

    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->
        repository.fetchItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

請注意,如果您需要更大的靈活性,您還可以使用 transformLatest 并顯式發出數據項:

    val result = userId.transformLatest { newUserId ->
        emit(Result.LoadingData)
        emit(repository.fetchItem(newUserId))
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser // Note the different Loading states
    )
5、觀察帶參數的數據流

現在讓我們讓這個更具響應性的例子。 數據不是獲取的,而是觀察到的,因此我們將數據源中的更改自動傳播到 UI。

繼續我們的例子:我們沒有在數據源上調用 fetchItem,而是使用一個假設的 observeItem 操作符,它返回一個 Flow。

使用 LiveData,您可以將流轉換為 LiveData 并發出所有更新:


觀察帶有參數的流 (LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: LiveData<String?> = 
        authManager.observeUser().map { user -> user.id }.asLiveData()

    val result = userId.switchMap { newUserId ->
        repository.observeItem(newUserId).asLiveData()
    }
}

或者,最好使用 flatMapLatest 組合兩個流,并僅將輸出轉換為 LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id }

    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.asLiveData()
}

Flow 實現類似,但沒有 LiveData 轉換:


觀察帶有參數的流 (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {
    private val userId: Flow<String?> = 
        authManager.observeUser().map { user -> user?.id }

    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.LoadingUser
    )
}

每當用戶更改或存儲庫中的用戶數據更改時,公開的 StateFlow 都會收到更新。

5、組合多個來源:MediatorLiveData -> Flow.combine

MediatorLiveData 可讓您觀察一個或多個更新源(LiveData 可觀察對象)并在它們獲得新數據時執行某些操作。 通常,您更新 MediatorLiveData 的值:

val liveData1: LiveData<Int> = ...
val liveData2: LiveData<Int> = ...

val result = MediatorLiveData<Int>()

result.addSource(liveData1) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}
result.addSource(liveData2) { value ->
    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))
}

Flow 等價寫法更直接:

val flow1: Flow<Int> = ...
val flow2: Flow<Int> = ...

val result = combine(flow1, flow2) { a, b -> a + b }

你也可以使用 combineTransform 函數, 或者 zip.

配置暴露的 StateFlow(stateIn 操作符)

我們之前使用 stateIn 將常規流轉換為 StateFlow,但它需要一些配置。 如果你現在不想詳細介紹,只需要復制粘貼,我推薦這種組合:

val result: StateFlow<Result<UiState>> = someFlow
    .stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )

但是,如果您不確定這個看似隨機的 5 秒啟動參數,請繼續閱讀。

stateIn 有 3 個參數(來自文檔):

@param scope 開始共享的協程范圍。
@param 啟動了控制何時開始和停止共享的策略。
@param initialValue 狀態流的初始值。
當使用帶有 replayExpirationMillis 參數的 [SharingStarted.WhileSubscribed] 策略重置狀態流時,也會使用此值。

開始可以采用 3 個值

  • Lazily:在第一個訂閱者出現時開始,在作用域取消時停止。
  • Eagerly:立即開始并在作用域取消時停止
  • WhileSubscribed這很復雜

對于一次性操作,您可以使用 Lazily 或 Eagerly。 但是,如果您正在觀察其他流程,則應該使用 WhileSubscribed 來執行小而重要的優化,如下所述。

WhileSubscribed 策略

WhileSubscribed 在沒有收集器時取消上游流。 使用 stateIn 創建的 StateFlow 向 View 公開數據,但它也在觀察來自其他層或應用程序(上游)的流。保持這些流處于活動狀態可能會導致資源浪費,例如,如果它們繼續從其他來源(如數據庫連接、硬件傳感器等)讀取數據。當你的應用進入后臺時,你應該做一個好公民并停止這些協程

WhileSubscribed 有兩個參數:

public fun WhileSubscribed(
    stopTimeoutMillis: Long = 0,
    replayExpirationMillis: Long = Long.MAX_VALUE
)

Stop timeout

從它的文檔:

stopTimeoutMillis配置最后一個訂閱者消失和上游流停止之間的延遲(以毫秒為單位)。 它默認為零(立即停止)。

這很有用,因為如果視圖停止偵聽幾分之一秒,您不想取消上游流。 這一直發生——例如,當用戶旋轉設備并且視圖被快速連續地破壞和重新創建時。

liveData 協程構建器中的解決方案是添加 5 秒的延遲,如果沒有訂閱者,協程將在此后停止。 WhileSubscribed(5000) 正是這樣做的:

class MyViewModel(...) : ViewModel() {
    val result = userId.mapLatest { newUserId ->
        repository.observeItem(newUserId)
    }.stateIn(
        scope = viewModelScope, 
        started = WhileSubscribed(5000), 
        initialValue = Result.Loading
    )
}

這種方法檢查所有框:

  • 當用戶將您的應用程序發送到后臺時,來自其他層的更新將在 5 秒后停止,從而節省電量。
  • 最新的值仍然會被緩存,這樣當用戶回到它時,視圖會立即有一些數據。
  • 訂閱重新啟動,新值將出現,可用時刷新屏幕。

重播到期

如果您不希望用戶在他們離開太久后看到陳舊數據并且您更喜歡顯示加載屏幕,請查看 WhileSubscribed 中的 replayExpirationMillis 參數。 在這種情況下它非常方便,而且還節省了一些內存,因為緩存的值會恢復到 stateIn 中定義的初始值。 返回應用程序不會那么快,但您不會顯示舊數據。

replayExpirationMillis——配置共享協程停止和重置重放緩存之間的延遲(以毫秒為單位)(這使得 shareIn 操作符的緩存為空,并將緩存值重置為 stateIn 操作符的原始初始值)。 它默認為 Long.MAX_VALUE(永遠保持重放緩存,從不重置緩沖區)。 使用零值立即使緩存過期。

從視圖中觀察 StateFlow

到目前為止,我們已經看到,讓 ViewModel 中的 StateFlows 知道View已經不再監聽是非常重要的。 然而,與生命周期相關的所有事情一樣,事情并沒有那么簡單。

為了收集流,您需要一個協程。 Activities和Fragments提供了一堆協程構建器:

  • Activity.lifecycleScope.launch:立即啟動協程,活動銷毀時取消協程。

  • Fragment.lifecycleScope.launch:立即啟動協程,并在片段銷毀時取消協程。

  • Fragment.viewLifecycleOwner.lifecycleScope.launch:立即啟動協程,并在片段的視圖生命周期被銷毀時取消協程。 如果您正在修改 UI,您應該使用視圖生命周期。

LaunchWhenStarted, launchWhenResumed…

稱為launchWhenX 的特殊版本的launch 將等到lifecycleOwner 處于X 狀態并在lifecycleOwner 低于X 狀態時暫停協程。 重要的是要注意,在其生命周期所有者被銷毀之前,它們不會取消協程

使用“launch/launchWhenX”收集流是不安全的

在應用程序處于后臺時接收更新可能會導致崩潰,這可以通過暫停視圖中的集合來解決。 但是,當應用程序在后臺時,上游流會保持活動狀態,這可能會浪費資源。

這意味著到目前為止我們為配置 StateFlow 所做的一切都將毫無用處; 然而,現在有一個新的 API。

lifecycle.repeatOnLifecycle

這個新的協程構建器(可從lifecycle-runtime-ktx 2.4.0-alpha01 獲得)正是我們所需要的:它在特定狀態下啟動協程,并在生命周期所有者低于它時停止它們。

不同的流量采集方式

例如,在一個Fragment中:

onCreateView(...) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {
            myViewModel.myUiState.collect { ... }
        }
    }
}

這將在 Fragment 的視圖開始時開始收集,將繼續通過 RESUMED,并在返回到 STOPPED 時停止。
點擊閱讀相關的全部介紹 A safer way to collect flows from Android UIs

將 repeatOnLifecycle API 與上面的 StateFlow 指南結合在一起,可以在充分利用設備資源的同時獲得最佳性能。

StateFlow 使用 WhileSubscribed(5000) 公開并使用 repeatOnLifecycle(STARTED) 收集

警告:StateFlow support recently added to Data Binding 目前使用*launchWhenCreated*來收集更新,在達到穩定之后將會采用*repeatOnLifecycle*

對于數據綁定,您應該在任何地方使用 Flows 并簡單地添加 asLiveData() 以將它們公開給視圖。 數據綁定將在 Lifecycle-runtime-ktx 2.4.0 穩定后更新。

總結

從 ViewModel 公開數據并從視圖收集數據的最佳方法是:

  • 使用 WhileSubscribed 策略公開 StateFlow 并設置超時。[例子]
  • 使用 repeatOnLifecycle 收集。 [例子]

任何其他組合都會使上游 Flows 保持活動狀態,從而浪費資源:

  • 使用 WhileSubscribed 公開并在生命周期范圍內使用launch/launchWhenX收集
  • 使用Lazily/Eagerly公開并使用 repeatOnLifecycle 收集

當然,如果您不需要 Flow 的全部功能……只需使用 LiveData。 :)

引用自Migrating from LiveData to Kotlin’s Flow

最后編輯于
©著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
37人點贊
兩三行代碼No evil<br>Focus<br>Do right thing<br>Simple is...
總資產8共写了5.1W字获得92个赞共37个粉丝
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容