Android開發(fā)實(shí)踐雅倒,使用 Kotlin Flow 構(gòu)建數(shù)據(jù)流 "管道"

Flow 是一種基于流的編程模型,本文我們將向大家介紹響應(yīng)式編程以及其在 Android 開發(fā)中的實(shí)踐攀芯,您將了解到如何將生命周期屯断、旋轉(zhuǎn)及切換到后臺等狀態(tài)綁定到 Flow 中文虏,并且測試它們是否能按照預(yù)期執(zhí)行

使用 Kotlin Flow 構(gòu)建數(shù)據(jù)流 "管道"

單向數(shù)據(jù)流

加載數(shù)據(jù)流的過程

每款 Android 應(yīng)用都需要以某種方式收發(fā)數(shù)據(jù)侣诺,比如從數(shù)據(jù)庫獲取用戶名、從服務(wù)器加載文檔氧秘,以及對用戶進(jìn)行身份驗(yàn)證等年鸳。接下來,我們將介紹如何將數(shù)據(jù)加載到 Flow丸相,然后經(jīng)過轉(zhuǎn)換后暴露給視圖進(jìn)行展示搔确。

為了大家更方便地理解 Flow,我們以 Pancho (潘喬) 的故事來展開灭忠。當(dāng)住在山上的 Pancho 想從湖中獲取淡水時(shí)膳算,會像大多數(shù)新手一開始一樣,拿個(gè)水桶走到湖邊取水弛作,然后再走回來涕蜂。

山上的 Pancho但有時(shí) Pahcho 不走運(yùn),走到湖邊時(shí)發(fā)現(xiàn)湖水已經(jīng)干涸映琳,于是就不得不再去別處尋找水源机隙。發(fā)生了幾次這種情況后,Pancho 意識到萨西,搭建一些基礎(chǔ)設(shè)施可以解決這個(gè)問題有鹿。于是他在湖邊安裝了一些管道,當(dāng)湖中有水時(shí)谎脯,只用擰開水龍頭就能取到水葱跋。知道了如何安裝管道,就能很自然地想到從多個(gè)水源地把管道組合,這樣一來 Pancho 就不必再檢查湖水是否已經(jīng)干涸娱俺。

鋪設(shè)管道在 Android 應(yīng)用中您可以簡單地在每次需要時(shí)請求數(shù)據(jù)际看,例如我們可以使用掛起函數(shù)來實(shí)現(xiàn)在每次視圖啟動時(shí)向 ViewModel 請求數(shù)據(jù),而后 ViewModel 又向數(shù)據(jù)層請求數(shù)據(jù)矢否,接下來這一切又在相反的方向上發(fā)生仲闽。不過這樣過了一段時(shí)間之后,像 Pancho 這樣的開發(fā)者們往往會想到僵朗,其實(shí)有必要投入一些成本來構(gòu)建一些基礎(chǔ)設(shè)施赖欣,我們就可以不再請求數(shù)據(jù)而改為觀察數(shù)據(jù)。觀察數(shù)據(jù)就像安裝取水管道一樣验庙,部署完成后對數(shù)據(jù)源的任何更新都將自動向下流動到視圖中顶吮,Pancho 再也不用走到湖邊去了。

傳統(tǒng)的請求數(shù)據(jù)與單向數(shù)據(jù)流

響應(yīng)式編程

我們將這類觀察者會自動對被觀察者對象的變化而作出反應(yīng)的系統(tǒng)稱之為響應(yīng)式編程粪薛,它的另一個(gè)設(shè)計(jì)要點(diǎn)是保持?jǐn)?shù)據(jù)只在一個(gè)方向上流動悴了,因?yàn)檫@樣更容易管理且不易出錯(cuò)。某個(gè)示例應(yīng)用界面的 "數(shù)據(jù)流動" 如下圖所示违寿,身份認(rèn)證管理器會告訴數(shù)據(jù)庫用戶已登錄湃交,而數(shù)據(jù)庫又必須告訴遠(yuǎn)程數(shù)據(jù)源來加載一組不同的數(shù)據(jù);與此同時(shí)這些操作在獲取新數(shù)據(jù)時(shí)都會告訴視圖顯示一個(gè)轉(zhuǎn)圈的加載圖標(biāo)藤巢。對此我想說這雖然是可行的搞莺,但容易出現(xiàn)錯(cuò)誤。

錯(cuò)綜復(fù)雜的 "數(shù)據(jù)流動"

更好的方式則是讓數(shù)據(jù)只在一個(gè)方向上流動掂咒,并創(chuàng)建一些基礎(chǔ)設(shè)施 (像 Pancho 鋪設(shè)管道那樣) 來組合和轉(zhuǎn)換這些數(shù)據(jù)流才沧,這些管道可以隨著狀態(tài)的變化而修改,比如在用戶退出登錄時(shí)重新安裝管道绍刮。

單向數(shù)據(jù)綁定

使用 Flow

可以想象對于這些組合和轉(zhuǎn)換來說温圆,我們需要一個(gè)成熟的工具來完成這些操作。在本文中我們將使用 Kotlin Flow 來實(shí)現(xiàn)孩革。Flow 并不是唯一的數(shù)據(jù)流構(gòu)建器岁歉,不過得益于它是協(xié)程的一部分并且得到了很好的支持。我們剛才一直用作比喻的水流嫉戚,在協(xié)程庫里稱之為 Flow 類型刨裆,我們用泛形 T 來指代數(shù)據(jù)流承載的用戶數(shù)據(jù)或者頁面狀態(tài)等任何類型。

生產(chǎn)者和消費(fèi)者

生產(chǎn)者會將數(shù)據(jù) emit (發(fā)送) 到數(shù)據(jù)流中彬檀,而消費(fèi)者則從數(shù)據(jù)流中 collect (收集) 這些數(shù)據(jù)帆啃。在 Android 中數(shù)據(jù)源或存儲區(qū)通常是應(yīng)用數(shù)據(jù)的生產(chǎn)者;消費(fèi)者則是視圖窍帝,它會把數(shù)據(jù)顯示在屏幕上努潘。

大多數(shù)情況下您都無需自行創(chuàng)建數(shù)據(jù)流,因?yàn)閿?shù)據(jù)源中依賴的庫,例如 DataStore疯坤、Retrofit报慕、Room 或 WorkManager 等常見的庫都已經(jīng)與協(xié)程及 Flow 集成在一起了。這些庫就像是水壩压怠,它們使用 Flow 來提供數(shù)據(jù)眠冈,您無需了解數(shù)據(jù)是如何生成的,只需 "接入管道" 即可菌瘫。

提供 Flow 支持的庫

我們來看一個(gè) Room 的例子蜗顽。您可以通過導(dǎo)出指定類型的數(shù)據(jù)流來獲取數(shù)據(jù)庫中發(fā)生變更的通知。在本例中雨让,Room 庫是生產(chǎn)者雇盖,它會在每次查詢后發(fā)現(xiàn)有更新時(shí)發(fā)送內(nèi)容。

@DAO
interface CodelabsDAO {
 
    @Query("SELECT * FROM codelabs")
    fun getAllCodelabs(): Flow<List<Codelab>>
}

創(chuàng)建 Flow

如果您要自己創(chuàng)建數(shù)據(jù)流栖忠,有一些方案可供選擇崔挖,比如數(shù)據(jù)流構(gòu)建器。假設(shè)我們處于 UserMessagesDataSource 中庵寞,當(dāng)您希望頻繁地在應(yīng)用內(nèi)檢查新消息時(shí)狸相,可以將用戶消息暴露為消息列表類型的數(shù)據(jù)流。我們使用數(shù)據(jù)流構(gòu)建器來創(chuàng)建數(shù)據(jù)流皇帮,因?yàn)?Flow 是在協(xié)程上下文環(huán)境中運(yùn)行的卷哩,它以掛起代碼塊作為參數(shù)蛋辈,這也意味著它能夠調(diào)用掛起函數(shù)属拾,我們可以在代碼塊中使用 while(true) 來循環(huán)執(zhí)行我們的邏輯。

在示例代碼中冷溶,我們首先從 API 獲取消息渐白,然后使用 emit 掛起函數(shù)將結(jié)果添加到 Flow 中,這將掛起協(xié)程直到收集器接收到數(shù)據(jù)項(xiàng)逞频,最后我們將協(xié)程掛起一段時(shí)間纯衍。在 Flow 中,操作會在同一個(gè)協(xié)程中順序執(zhí)行苗胀,使用 while(true) 循環(huán)可以讓 Flow 持續(xù)獲取新消息直到觀察者停止收集數(shù)據(jù)襟诸。傳遞給數(shù)據(jù)流構(gòu)建器的掛起代碼塊通常被稱為 "生產(chǎn)者代碼塊"。

class UserMessagesDataSource(
    private val messagesApi: MessagesApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestMessages: Floa<List<Message>> = flow {
        white(true) {
            val userMessages = messagesApi.fetchLatestMessages()
            emit(userMessages) // 將結(jié)果發(fā)送給 Flow
            delay(refreshIntervalMs) // ? 掛起一段時(shí)間
        }
    }
}

轉(zhuǎn)換 Flow

在 Android 中基协,生產(chǎn)者和消費(fèi)者之間的層可以使用中間運(yùn)算符修改數(shù)據(jù)流來適應(yīng)下一層的要求歌亲。

在本例中,我們將 latestMessages 流作為數(shù)據(jù)流的起點(diǎn)澜驮,則可以使用 map 運(yùn)算符將數(shù)據(jù)轉(zhuǎn)換為不同的類型陷揪,例如我們可以使用 map lambda 表達(dá)式將來自數(shù)據(jù)源的原始消息轉(zhuǎn)換為 MessagesUiModel,這一操作可以更好地抽象當(dāng)前層級,每個(gè)運(yùn)算符都應(yīng)根據(jù)其功能創(chuàng)建一個(gè)新的 Flow 來發(fā)送數(shù)據(jù)悍缠。我們還可以使用 filter 運(yùn)算符過濾數(shù)據(jù)流來獲得包含重要通知的數(shù)據(jù)流卦绣。而 catch 運(yùn)算符則可以捕獲上游數(shù)據(jù)流中發(fā)生的異常,上游數(shù)據(jù)流是指在生產(chǎn)者代碼塊和當(dāng)前運(yùn)算符之間調(diào)用的運(yùn)算符產(chǎn)生的數(shù)據(jù)流飞蚓,而在當(dāng)前運(yùn)算符之后生成的數(shù)據(jù)流則被稱為下游數(shù)據(jù)流滤港。catch 運(yùn)算符還可以在有需要的時(shí)候再次拋出異常或者發(fā)送新值趴拧,我們在示例代碼中可以看到其在捕獲到 IllegalArgumentExceptions 時(shí)將其重新拋出蜗搔,并且在發(fā)生其他異常時(shí)發(fā)送一個(gè)空列表:


val importantUserMessages: Flow<MessageUiModel> = 
    userMessageDataSource.latestMessages
        .map { userMessage ->
            userMessages.toUiModel()
        }
        .filter { messageUiModel ->
            messagesUiModel.containsImportantNotifications()
        }
        .catch { e ->
            analytics.log("Error loading reserved event")
            if (e is IllegalArgumentException) throw e
            else emit(emptyList())
        }

收集 Flow

現(xiàn)在我們已經(jīng)了解過如何生成和修改數(shù)據(jù)流,接下來了解一下如何收集數(shù)據(jù)流八堡。收集數(shù)據(jù)流通常發(fā)生在視圖層樟凄,因?yàn)檫@是我們想要在屏幕上顯示數(shù)據(jù)的地方。

在本例中兄渺,我們希望列表中能夠顯示最新消息以便 Pancho 能夠了解最新動態(tài)缝龄。我們可以使用終端運(yùn)算符 collect 來監(jiān)聽數(shù)據(jù)流發(fā)送的所有值,collect 接收一個(gè)函數(shù)作為參數(shù)挂谍,每個(gè)新值都會調(diào)用該參數(shù)叔壤,并且由于它是一個(gè)掛起函數(shù),因此需要在協(xié)程中執(zhí)行口叙。

userMessages.collect { messages ->
    listAdapter.submitList(messages)
}

在 Flow 中使用終端運(yùn)算符將按需創(chuàng)建數(shù)據(jù)流并開始發(fā)送值炼绘,而相反的是中間操作符只是設(shè)置了一個(gè)操作鏈,其會在數(shù)據(jù)被發(fā)送到數(shù)據(jù)流時(shí)延遲執(zhí)行妄田。每次對 userMessages 調(diào)用 collect 時(shí)都會創(chuàng)建一個(gè)新的數(shù)據(jù)流俺亮,其生產(chǎn)者代碼塊將根據(jù)自己的時(shí)間間隔開始刷新來自 API 的消息。在協(xié)程中我們將這種按需創(chuàng)建并且只有在被觀察時(shí)才會發(fā)送數(shù)據(jù)的數(shù)據(jù)流稱之為冷流 (Cold Stream)疟呐。

在 Android 視圖上收集數(shù)據(jù)流

在 Android 的視圖中收集數(shù)據(jù)流要注意兩點(diǎn)脚曾,第一是在后臺運(yùn)行時(shí)不應(yīng)浪費(fèi)資源,第二是配置變更启具。

安全收集

假設(shè)我們在 MessagesActivity 中本讥,如果希望在屏幕上顯示消息列表,則應(yīng)該當(dāng)界面沒有顯示在屏幕上時(shí)停止收集鲁冯,就像是 Pancho 在刷牙或者睡覺時(shí)應(yīng)該關(guān)上水龍頭一樣拷沸。我們有多種具有生命周期感知能力的方案,來實(shí)現(xiàn)當(dāng)信息不在屏幕上展示就不從數(shù)據(jù)流中收集信息的功能薯演,比如 androidx.lifecycle:lifecycle-runtime-ktx 包中的 Lifecycle.repeatOnLifecycle(state) 和 Flow<T>.flowWithLifecycle(lifecycle, state)撞芍。您還可以在 ViewModel 中使用 androidx.lifecycle:lifecycle-livedata-ktx 包里的 Flow<T>.asLiveData(): LiveData 將數(shù)據(jù)流轉(zhuǎn)換為 LiveData,這樣就可以像往常一樣使用 LiveData 來實(shí)現(xiàn)這件事情涣仿。不過為了簡單起見勤庐,這里推薦使用 repeatOnLifecycle 從界面層收集數(shù)據(jù)流示惊。

repeatOnLifecycle 是一個(gè)接收 Lifecycle.State 作為參數(shù)的掛起函數(shù),該 API 具有生命周期感知能力愉镰,所以能夠在當(dāng)生命周期進(jìn)入響應(yīng)狀態(tài)時(shí)自動使用傳遞給它的代碼塊啟動新的協(xié)程米罚,并且在生命周期離開該狀態(tài)時(shí)取消該協(xié)程。在上面的例子中丈探,我們使用了 Activity 的 lifecycleScope 來啟動協(xié)程录择,由于 repeatOnLifecycle 是掛起函數(shù),所以它需要在協(xié)程中被調(diào)用碗降。最佳實(shí)踐是在生命周期初始化時(shí)調(diào)用該函數(shù)隘竭,就像上面的例子中我們在 Activity 的 onCreate 中調(diào)用一樣:

import androidx.lifecycle.repeatOnLifecycle
 
class MessagesActivity : AppCompatActivity() {
 
    val viewModel: MessagesViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
           
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED)
                    viewModel.userMessages.collect { messages ->
                        listAdapter.submitList(messages)
                    }
                }
                // 協(xié)程將會在 lifecycle 進(jìn)入 DESTROYED 后被恢復(fù)
            }
    }
}

repeatOnLifecycle 的可重啟行為充分考慮了界面的生命周期,不過需要注意的是讼渊,直到生命周期進(jìn)入 DESTROYED动看,調(diào)用 repeatOnLifecycle 的協(xié)程都不會恢復(fù)執(zhí)行,因此如果您需要從多個(gè)數(shù)據(jù)流中進(jìn)行收集爪幻,則應(yīng)在 repeatOnLifecycle 代碼塊內(nèi)多次使用 launch 來創(chuàng)建協(xié)程:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
 
            launch {
                viewModel.userMessages.collect { … }
            }
 
            launch {
                otherFlow.collect { … }
            }
    }
}

如果只需從一個(gè)數(shù)據(jù)流中進(jìn)行收集菱皆,則可使用 flowWithLifecycle 來收集數(shù)據(jù),它能夠在生命周期進(jìn)入目標(biāo)狀態(tài)時(shí)發(fā)送數(shù)據(jù)挨稿,并在離開目標(biāo)狀態(tài)時(shí)取消內(nèi)部的生產(chǎn)者:


lifecycleScope.launch {
    viewModel.userMessages
        .flowWithLifecycle(lifecycle, State.STARTED)
        .collect { messages ->
            listAdapter.submitList(messages)
        }
}

為了能夠直觀地展示具體的運(yùn)作過程仇轻,我們來探索一下此 Activity 的生命周期,首先是創(chuàng)建完成并向用戶可見奶甘;接下來用戶按下了主屏幕按鈕將應(yīng)用退到后臺篷店,此時(shí) Activity 會收到 onStop 信號;當(dāng)重新打開應(yīng)用時(shí)又會調(diào)用 onStart臭家。如果您調(diào)用 repeatOnLifecycle 并傳入 STARTED 狀態(tài)疲陕,界面就只會在屏幕上顯示時(shí)收集數(shù)據(jù)流發(fā)出的信號,并且在應(yīng)用轉(zhuǎn)到后臺時(shí)取消收集侣监。

Activity 的生命周期

repeatOnLifecycle 和 flowWithLifecycle 是 lifecycle-runtime-ktx 庫在 2.4.0 穩(wěn)定版中新增的 API鸭轮,在沒有這些 API 之前您可能已經(jīng)以其他方式從 Android 界面中收集數(shù)據(jù)流,例如像上面的代碼一樣直接從 lifecycleScope.launch 啟動的協(xié)程中收集橄霉,雖然這樣看起來也能工作但不一定安全,因?yàn)檫@種方式將持續(xù)從數(shù)據(jù)流中收集數(shù)據(jù)并更新界面元素邑蒋,即便是應(yīng)用退出到后臺時(shí)也一樣姓蜂。如果使用 launchWhenStarted 替代它的話娱据,情況會稍微好一些烂完,因?yàn)樗鼤谔幱诤笈_時(shí)將收集掛起。但這樣會在讓數(shù)據(jù)流生產(chǎn)者保持活躍狀態(tài)缘眶,有可能會在后臺持續(xù)發(fā)出不需要在屏幕上顯示的數(shù)據(jù)項(xiàng)卿堂,從而將內(nèi)存占滿束莫。由于界面并不知道數(shù)據(jù)流生產(chǎn)者的實(shí)現(xiàn)方式懒棉,所以最好謹(jǐn)慎一些,使用 repeatOnLifecycle 或 flowWithLifecycle 來避免界面在處于后臺時(shí)收集數(shù)據(jù)或保持?jǐn)?shù)據(jù)流生產(chǎn)者處于活躍狀態(tài)览绿。下面是一段不安全的使用方式示例:


class MessagesActivity : AppCompatActivity() {
 
    val viewModel: MessagesViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
 
            // ? 危險(xiǎn)的操作
            lifecycleScope.launch {
                viewModel.userMessage.collect { messages ->
                    listAdapter.submitList(messages)
                }
            }
 
            // ? 危險(xiǎn)的操作
            LifecycleCoroutineScope.launchWhenX {
                flow.collect { … }
            }
    }
}

配置變更

當(dāng)您向視圖暴露數(shù)據(jù)流時(shí)策严,必須要考慮到您正在嘗試在具有不同生命周期的兩個(gè)元素之間傳遞數(shù)據(jù),并不是所有生命周期都會出現(xiàn)問題饿敲,但在 Activity 和 Fragment 的生命周期里會比較棘手妻导。當(dāng)設(shè)備旋轉(zhuǎn)或者接收到配置變更時(shí),所有的 Activity 都可能會重啟但 ViewModel 卻能被保留怀各,因此您不能把任意數(shù)據(jù)流都簡單地從 ViewModel 中暴露出來倔韭。

旋轉(zhuǎn)屏幕會重建 Activity 但能夠保留 ViewModel

以如下代碼中的冷流為例,由于每次收集冷流時(shí)它都會重啟瓢对,所以在設(shè)備旋轉(zhuǎn)之后會再次調(diào)用 repository.fetchItem()寿酌。我們需要某種緩沖區(qū)機(jī)制來保障無論重新收集多少次都可以保持?jǐn)?shù)據(jù),并在多個(gè)收集器之間共享數(shù)據(jù)硕蛹,而 StateFlow 正是為了此用途而設(shè)計(jì)的份名。在我們的湖泊比喻中,StateFlow 就好比水箱妓美,即使沒有收集器它也能持有數(shù)據(jù)僵腺。因?yàn)樗梢远啻伪皇占阅軌蚍判牡貙⑵渑c Activity 或 Fragment 一起使用壶栋。

val result: Flow<Result<UiState>> = flow {
    emit(repository.fetchItem())
}

您可以使用 StateFlow 的可變版本辰如,并隨時(shí)根據(jù)需要在協(xié)程中更新它的值,但這樣做可能不太符合響應(yīng)式編程的風(fēng)格贵试,如下代碼所示:

private val _myUiState = MutableStateFlow<MyUiState>()
 
val myUiState: StateFlow<MyUiState> = _myUiState
 
init {
    viewModelScope.launch {
        _muUiState.value = Result.Loading
        _myUiState.value = repository.fetchStuff()
    }
}

Pancho 會建議您將各種類型的數(shù)據(jù)流都轉(zhuǎn)換為 StateFlow 來改進(jìn)這個(gè)問題琉兜,這樣 StateFlow 將接收來自上游數(shù)據(jù)流的所有更新并存儲最新的值,并且收集器的數(shù)量可以是 0 至任意多個(gè)毙玻,因此非常適合與 ViewModel 一起使用豌蟋。當(dāng)然,除此之外還有一些其他類型的 Flow桑滩,但推薦您使用 StateFlow梧疲,因?yàn)槲覀兛梢詫λM(jìn)行非常精確的優(yōu)化。

將任意數(shù)據(jù)流轉(zhuǎn)換為 StateFlow要將數(shù)據(jù)流轉(zhuǎn)換為 StateFlow 可以使用 stateIn 運(yùn)算符运准,它需要傳入三個(gè)參數(shù): initinalValue幌氮、scope 及 started。其中 initialValue 是因?yàn)?StateFlow 必須有值胁澳;而協(xié)程 scope 則是用于控制何時(shí)開始共享该互,在上面的例子中我們使用了 viewModelScope;最后的 started 是個(gè)有趣的參數(shù)韭畸,我們后面會聊到 WhileSubscribed(5000) 的作用宇智,先看這部分的代碼:

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

我們來看看這兩個(gè)場景: 第一種場景是旋轉(zhuǎn)蔓搞,在該場景中 Activity (也就是數(shù)據(jù)流收集器) 在短時(shí)間內(nèi)被銷毀然后重建;第二個(gè)場景是回到主屏幕随橘,這將會使我們的應(yīng)用進(jìn)入后臺喂分。在旋轉(zhuǎn)場景中我們不希望重啟任何數(shù)據(jù)流以便盡可能快地完成過渡,而在回到主屏幕的場景中我們則希望停止所有數(shù)據(jù)流以便節(jié)省電量和其他資源太防。

我們可以通過設(shè)置超時(shí)時(shí)間來正確判斷不同的場景妻顶,當(dāng)停止收集 StateFlow 時(shí),不會立即停止所有上游數(shù)據(jù)流蜒车,而是會等待一段時(shí)間讳嘱,如果在超時(shí)前再次收集數(shù)據(jù)則不會取消上游數(shù)據(jù)流,這就是 WhileSubscribed(5000) 的作用酿愧。當(dāng)設(shè)置了超時(shí)時(shí)間后沥潭,如果按下主屏幕按鈕會讓視圖立即結(jié)束收集,但 StateFlow 會經(jīng)過我們設(shè)置的超時(shí)時(shí)間之后才會停止其上游數(shù)據(jù)流嬉挡,如果用戶再次打開應(yīng)用則會自動重啟上游數(shù)據(jù)流钝鸽。而在旋轉(zhuǎn)場景中視圖只停止了很短的時(shí)間,無論如何都不會超過 5 秒鐘庞钢,因此 StateFlow 并不會重啟拔恰,所有的上游數(shù)據(jù)流都將會保持在活躍狀態(tài),就像什么都沒有發(fā)生一樣可以做到即時(shí)向用戶呈現(xiàn)旋轉(zhuǎn)后的屏幕基括。

設(shè)置超時(shí)時(shí)間來應(yīng)對不同的場景總的來說颜懊,建議您使用 StateFlow 來通過 ViewModel 暴露數(shù)據(jù)流,或者使用 asLiveData 來實(shí)現(xiàn)同樣的目的风皿,關(guān)于 StateFlow 或其父類 SharedFlow 的更多詳細(xì)信息河爹,請參閱: StateFlow 和 SharedFlow

測試數(shù)據(jù)流

測試數(shù)據(jù)流可能會比較復(fù)雜,因?yàn)橐幚淼膶ο笫橇魇綌?shù)據(jù)桐款,這里介紹在兩個(gè)不同的場景中有用的小技巧:首先是第一個(gè)場景咸这,被測單元依賴了數(shù)據(jù)流,那對此類場景進(jìn)行測試最簡單的方法就是用模擬生產(chǎn)者替代依賴項(xiàng)魔眨。在本例中媳维,您可以對這個(gè)模擬源進(jìn)行編程以對不同的測試用例發(fā)送其所需要的內(nèi)容。您可以像上面的例子一樣實(shí)現(xiàn)一個(gè)簡單的冷流冰沙,測試本身會對受測對象的輸出進(jìn)行斷言侨艾,輸出的內(nèi)容可以是數(shù)據(jù)流或其他任何類型。

被測單元依賴數(shù)據(jù)流的測試技巧模擬被測單元所依賴的數(shù)據(jù)流:


class MyFakeRepository : MyRepository {
    fun observeCount() = flow {
        emit(ITEM_1)
    }
}

如果受測單元暴露一個(gè)數(shù)據(jù)流拓挥,并且您希望驗(yàn)證該值或一系列值,那么您可以通過多種方式收集它們袋励。您可以對數(shù)據(jù)流調(diào)用 first() 方法以進(jìn)行收集并在接收到第一個(gè)數(shù)據(jù)項(xiàng)后停止收集侥啤。您還可以調(diào)用 take(5) 并使用 toList 終端操作符來收集恰好 5 條消息当叭,這種方法可能非常有幫助。

測試數(shù)據(jù)流的技巧測試數(shù)據(jù)流:


@Test
fun myTest() = runBlocking {
 
    // 收集第一個(gè)數(shù)據(jù)然后停止收集
    val firstItem = repository.counter.first()
 
    // 收集恰好 5 條消息
    val first = repository.messages.take(5).toList()
}

回顧

感謝閱讀本文盖灸,希望您通過本文內(nèi)容已經(jīng)了解到為什么響應(yīng)式架構(gòu)值得投資蚁鳖,以及如何使用 Kotlin Flow 構(gòu)建您的基礎(chǔ)設(shè)施。文末提供了有關(guān)這方面的資料赁炎,包括涵蓋基礎(chǔ)知識的指南以及深入探討某些主題的文章醉箕。另外您還可以通過 Google I/O 應(yīng)用了解這些內(nèi)容的詳細(xì)信息,我們在早些時(shí)候?yàn)槠涓铝撕芏嘤嘘P(guān)數(shù)據(jù)流的內(nèi)容徙垫。

最后

給大家分享一份谷歌開源的《史上最詳Android版kotlin協(xié)程入門進(jìn)階實(shí)戰(zhàn)指南》讥裤,希望可以幫助大家用最短時(shí)間學(xué)習(xí) Kotlin攜程。教程通俗易懂姻报,實(shí)例豐富己英,既有基礎(chǔ)知識,也有進(jìn)階技能吴旋,能夠幫助讀者快速入門進(jìn)階损肛,是你學(xué)習(xí)Kotlin的葵花寶典,快收藏起來H偕治拿!需要的朋友可以點(diǎn)擊這里免費(fèi)領(lǐng)取。

第一章 Kotlin協(xié)程的基礎(chǔ)介紹

  • 協(xié)程是什么
  • 什么是Job 笆焰、Deferred 劫谅、協(xié)程作用域
  • Kotlin協(xié)程的基礎(chǔ)用法

第二章 kotlin協(xié)程的關(guān)鍵知識點(diǎn)初步講解

  • 協(xié)程調(diào)度器
  • 協(xié)程調(diào)度器
  • 協(xié)程啟動模式
  • 協(xié)程作用域
  • 掛起函數(shù)

第三章 kotlin協(xié)程的異常處理

  • 協(xié)程異常的產(chǎn)生流程
  • 協(xié)程的異常處理

第四章 kotlin協(xié)程在Android中的基礎(chǔ)應(yīng)用

  • Android使用kotlin協(xié)程
  • 在Activity與Framgent中使用協(xié)程
  • ViewModel中使用協(xié)程
  • 其他環(huán)境下使用協(xié)程

第五章 kotlin協(xié)程的網(wǎng)絡(luò)請求封裝

  • 協(xié)程的常用環(huán)境
  • 協(xié)程在網(wǎng)絡(luò)請求下的封裝及使用
  • 高階函數(shù)方式
  • 多狀態(tài)函數(shù)返回值方式
  • 直接返回值的方式

第六章 深入kotlin協(xié)程原理(一)

  • suspend 的花花腸子
  • 藏在身后的- Continuation
  • 村里的希望- SuspendLambda

第七章 深入kotlin協(xié)程原理(二)

  • 協(xié)程的那些小秘密
  • 協(xié)程的創(chuàng)建過程
  • 協(xié)程的掛起與恢復(fù)
  • 協(xié)程的執(zhí)行與狀態(tài)機(jī)

第八章 Kotlin Jetpack 實(shí)戰(zhàn)

  • 從一個(gè)膜拜大神的 Demo 開始
  • Kotlin 寫 Gradle 腳本是一種什么體驗(yàn)?
  • Kotlin 編程的三重境界
  • Kotlin 高階函數(shù)
  • Kotlin 泛型
    -Kotlin 擴(kuò)展
  • Kotlin 委托
  • 協(xié)程“不為人知”的調(diào)試技巧
  • 圖解協(xié)程原理

第九章 Kotlin + 協(xié)程 + Retrofit + MVVM優(yōu)雅的實(shí)現(xiàn)網(wǎng)絡(luò)請求

  • 項(xiàng)目配置
  • 實(shí)現(xiàn)思路
  • 協(xié)程實(shí)現(xiàn)
  • 協(xié)程 + ViewModel + LiveData實(shí)現(xiàn)
  • 后續(xù)優(yōu)化
  • 異常處理
  • 更新Retrofit 2.6.0

文章篇幅有限仙辟,內(nèi)容比較多同波,需要這份谷歌開源的《史上最詳Android版kotlin協(xié)程入門進(jìn)階實(shí)戰(zhàn)指南》完整版的朋友,點(diǎn)擊這里查看全部內(nèi)容保證100%免費(fèi)叠国。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末未檩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子粟焊,更是在濱河造成了極大的恐慌冤狡,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件项棠,死亡現(xiàn)場離奇詭異悲雳,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)香追,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門合瓢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人透典,你說我怎么就攤上這事晴楔《傥” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵税弃,是天一觀的道長纪岁。 經(jīng)常有香客問我,道長则果,這世上最難降的妖魔是什么幔翰? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮西壮,結(jié)果婚禮上遗增,老公的妹妹穿的比我還像新娘。我一直安慰自己茸时,他們只是感情好贡定,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著可都,像睡著了一般缓待。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上渠牲,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天旋炒,我揣著相機(jī)與錄音,去河邊找鬼签杈。 笑死瘫镇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的答姥。 我是一名探鬼主播铣除,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鹦付!你這毒婦竟也來了尚粘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敲长,失蹤者是張志新(化名)和其女友劉穎郎嫁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祈噪,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泽铛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辑鲤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盔腔。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铲觉,到底是詐尸還是另有隱情澈蝙,我是刑警寧澤吓坚,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布撵幽,位于F島的核電站,受9級特大地震影響礁击,放射性物質(zhì)發(fā)生泄漏盐杂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一哆窿、第九天 我趴在偏房一處隱蔽的房頂上張望链烈。 院中可真熱鬧,春花似錦挚躯、人聲如沸强衡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漩勤。三九已至,卻和暖如春缩搅,著一層夾襖步出監(jiān)牢的瞬間越败,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工硼瓣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留究飞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓堂鲤,卻偏偏與公主長得像亿傅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子瘟栖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容