從LiveData遷移到Kotlin Flow

響應(yīng)式的框架

RxJava:過于復(fù)雜、學(xué)習(xí)成本高

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

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

Flow:簡單的事情更難,復(fù)雜的事情更容易

LiveData擅長于暴露最近獲取的數(shù)據(jù),并且能夠結(jié)合Android的生命周期。后來我們了解到它也可以啟動協(xié)程并創(chuàng)建復(fù)雜的轉(zhuǎn)換,但這有點復(fù)雜。

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

1、使用可變數(shù)據(jù)持有者公開一次性操作的結(jié)果

這是經(jīng)典模式,您可以使用協(xié)程的結(jié)果來改變狀態(tài)持有者:

使用可變數(shù)據(jù)持有者 (LiveData) 公開一次性操作的結(jié)果
<!-- 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來達到相同的效果:

使用可變數(shù)據(jù)持有者 (StateFlow) 公開一次性操作的結(jié)果
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:

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

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

2、公開一次性操作的結(jié)果

這與前面的代碼片段等效,公開了沒有可變后備屬性的協(xié)程調(diào)用的結(jié)果。

對于 LiveData,我們?yōu)榇耸褂昧?liveData 協(xié)程構(gòu)建器:


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

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

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

公開一次性操作的結(jié)果 (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 轉(zhuǎn)換為 StateFlow 的 Flow 運算符。 現(xiàn)在讓我們相信這些參數(shù),因為我們稍后需要更多的復(fù)雜性來正確解釋它。

3、帶參數(shù)的一次性數(shù)據(jù)加載

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


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

使用 LiveData,您將執(zhí)行類似以下操作:

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 是一個轉(zhuǎn)換,它的主體被執(zhí)行,并且當 userId 改變時訂閱結(jié)果。

如果 userId 沒有理由成為 LiveData,那么更好的替代方法是將流與 Flow 結(jié)合起來,最后將公開的結(jié)果轉(zhuǎn)換為 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 執(zhí)行此操作看起來非常相似:


帶參數(shù)的一次性數(shù)據(jù)加載(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 并顯式發(fā)出數(shù)據(jù)項:

    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、觀察帶參數(shù)的數(shù)據(jù)流

現(xiàn)在讓我們讓這個更具響應(yīng)性的例子。 數(shù)據(jù)不是獲取的,而是觀察到的,因此我們將數(shù)據(jù)源中的更改自動傳播到 UI。

繼續(xù)我們的例子:我們沒有在數(shù)據(jù)源上調(diào)用 fetchItem,而是使用一個假設(shè)的 observeItem 操作符,它返回一個 Flow。

使用 LiveData,您可以將流轉(zhuǎn)換為 LiveData 并發(fā)出所有更新:


觀察帶有參數(shù)的流 (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 組合兩個流,并僅將輸出轉(zhuǎn)換為 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 實現(xiàn)類似,但沒有 LiveData 轉(zhuǎn)換:


觀察帶有參數(shù)的流 (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
    )
}

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

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

MediatorLiveData 可讓您觀察一個或多個更新源(LiveData 可觀察對象)并在它們獲得新數(shù)據(jù)時執(zhí)行某些操作。 通常,您更新 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 函數(shù), 或者 zip.

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

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

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

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

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

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

開始可以采用 3 個值

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

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

WhileSubscribed 策略

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

WhileSubscribed 有兩個參數(shù):

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

Stop timeout

從它的文檔:

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

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

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

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

這種方法檢查所有框:

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

重播到期

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

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

從視圖中觀察 StateFlow

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

為了收集流,您需要一個協(xié)程。 Activities和Fragments提供了一堆協(xié)程構(gòu)建器:

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

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

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

LaunchWhenStarted, launchWhenResumed…

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

使用“l(fā)aunch/launchWhenX”收集流是不安全的

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

這意味著到目前為止我們?yōu)榕渲?StateFlow 所做的一切都將毫無用處; 然而,現(xiàn)在有一個新的 API。

lifecycle.repeatOnLifecycle

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

不同的流量采集方式

例如,在一個Fragment中:

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

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

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

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

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

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

總結(jié)

從 ViewModel 公開數(shù)據(jù)并從視圖收集數(shù)據(jù)的最佳方法是:

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

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

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

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

引用自Migrating from LiveData to Kotlin’s Flow

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容