本文介紹了我們在開發(fā) 2019 Android 開發(fā)者峰會 (ADS) 應(yīng)用時總結(jié)整理的 Flow 最佳實踐 (應(yīng)用源碼已開源),我們將和大家共同探討應(yīng)用中的每個層級將如何處理數(shù)據(jù)流。
ADS 應(yīng)用的架構(gòu)遵守 Android 官方的推薦架構(gòu)指南,我們在其中引入了 Domain 層 (用以囊括各種 UseCases 類) 來幫助分離焦點,進而保持代碼的精簡、復(fù)用性、可測試性。
更多關(guān)于應(yīng)用架構(gòu)指南的分層設(shè)計 (Data 層、Domain 層、UI 層),請參考示例應(yīng)用 | Plaid 2.0 重構(gòu)。
如同許多 Android 應(yīng)用一樣,ADS 應(yīng)用從網(wǎng)絡(luò)或緩存懶加載數(shù)據(jù)。我們發(fā)現(xiàn),這種場景非常適合 Flow。掛起函數(shù) (suspend functions) 更適合于一次性操作。為了使用協(xié)程,我們將重構(gòu)分為兩次 commit 提交: 第一次遷移了一次性操作,第二次將其遷移至數(shù)據(jù)流。
在本文中,您將看到我們把應(yīng)用從 "在所有層級使用 LiveData",重構(gòu)為 "只在 View 和 ViewModel 間使用 LiveData 進行通訊,并在應(yīng)用的底層和 UserCase 層架構(gòu)中使用協(xié)程"。
優(yōu)先使用 Flow 來暴露數(shù)據(jù)流 (而不是 Channel)
您有兩種方法在協(xié)程中處理數(shù)據(jù)流: 一種是 Flow API,另一種是 Channel API。Channels 是一種同步原語,而 Flows 是為數(shù)據(jù)流模型所設(shè)計的: 它是訂閱數(shù)據(jù)流的工廠。不過我們可以使用 Channels 來支持 Flows,這一點我們稍后再說。
相較于 Channel,F(xiàn)low 更靈活,并提供了更明確的約束和更多操作符。
由于末端操作符 (terminal operator) 會觸發(fā)數(shù)據(jù)流的執(zhí)行,同時會根據(jù)生產(chǎn)者一側(cè)流操作來決定是成功完成操作還是拋出異常,因此 Flows 會自動地關(guān)閉數(shù)據(jù)流,您基本不會在生產(chǎn)者一側(cè)泄漏資源;而一旦 Channel 沒有正確關(guān)閉,生產(chǎn)者可能不會清理大型資源,因此 Channels 更容易造成資源泄漏。
應(yīng)用數(shù)據(jù)層負責(zé)提供數(shù)據(jù),通常是從數(shù)據(jù)庫中讀取,或從網(wǎng)絡(luò)獲取數(shù)據(jù),例如,示例是一個數(shù)據(jù)源接口,它提供了一個用戶事件數(shù)據(jù)流:
interface UserEventDataSource {
fun getObservableUserEvent(userId: String): Flow<UserEventResult>
}
如何將 Flow 應(yīng)用在您的 Android 應(yīng)用架構(gòu)中
1. UseCase 層和 Repository 層
介于 View/ViewModel 和數(shù)據(jù)源之間的層 (在我們的例子中是 UseCase 和 Repository) 通常需要合并來自多個查詢的數(shù)據(jù),或在 ViewModel 層使用之前轉(zhuǎn)化數(shù)據(jù)。就像 Kotlin sequences 一樣,F(xiàn)low 支持大量操作符來轉(zhuǎn)換數(shù)據(jù)。目前已經(jīng)有大量的可用的操作符,同時您也可以創(chuàng)建您自己的轉(zhuǎn)換器 (比如,使用 transform 操作符)。不過 Flow 在許多的操作符中暴露了 suspend lambda 表達式,因此在大多數(shù)情況下沒有必要通過自定義轉(zhuǎn)換來完成復(fù)雜任務(wù),可以直接在 Flow 中調(diào)用掛起函數(shù)。
在 ADS 應(yīng)用中,我們想將 UserEventResult 和 Repository 層中的會話數(shù)據(jù)進行綁定。我們利用 map 操作符來將一個 suspend lambda 表達式應(yīng)用在從數(shù)據(jù)源接收到的每一個 Flow 的值上:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
class DefaultSessionAndUserEventRepository(
private val userEventDataSource: UserEventDataSource,
private val sessionRepository: SessionRepository
) : SessionAndUserEventRepository {
override fun getObservableUserEvent(
userId: String?,
eventId: SessionId
): Flow<Result<LoadUserSessionUseCaseResult>> {
// 處理 userId
// 監(jiān)聽用戶事件,并將其與 Session 數(shù)據(jù)進行合并
return userEventDataSource.getObservableUserEvent(userId, eventId).map { userEventResult ->
val event = sessionRepository.getSession(eventId)
// 將 Session 和用戶數(shù)據(jù)進行合并,并傳遞結(jié)果
val userSession = UserSession(
event,
userEventResult.userEvent ?: createDefaultUserEvent(event)
)
Result.Success(LoadUserSessionUseCaseResult(userSession))
}
}
}
2. ViewModel
在利用 LiveData 執(zhí)行 UI ? ViewModel 通信時,ViewModel 層應(yīng)該利用末端操作符來消費來自數(shù)據(jù)層的數(shù)據(jù)流 (比如: collect、first 或者是 toList) 。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
// 真實代碼的簡化版
class SessionDetailViewModel(
private val loadUserSessionUseCase: LoadUserSessionUseCase,
...
): ViewModel() {
private fun listenForUserSessionChanges(sessionId: SessionId) {
viewModelScope.launch {
loadUserSessionUseCase(sessionId).collect { loadResult ->
}
}
}
}
如果您需要將 Flow 轉(zhuǎn)化為 LiveData,則可以使用 AndroidX lifecycle library 提供的 Flow.asLiveData() 擴展函數(shù) (extension function)。這個擴展函數(shù)非常便于使用,因為它共享了 Flow 的底層訂閱,同時根據(jù)觀察者的生命周期管理訂閱。此外,LiveData 可以為后續(xù)添加的觀察者提供最新的數(shù)據(jù),其訂閱在配置發(fā)生變更的時候依舊能夠生效。下面利用一段簡單的代碼來演示如何使用這個擴展函數(shù):
class SimplifiedSessionDetailViewModel(
private val loadUserSessionUseCase: LoadUserSessionUseCase,
...
): ViewModel() {
val sessions = loadUserSessionUseCase(sessionId).asLiveData()
}
特別說明: 這段代碼不是 ADS 應(yīng)用的,它只是用來演示如何使用 Flow.asLiveData()。
具體實現(xiàn)時,該在何時使用 BroadcastChannel 或者 Flow
回到數(shù)據(jù)源的實現(xiàn),要怎樣去實現(xiàn)之前暴露的 getObservableUserEvent 函數(shù)?我們考慮了兩種實現(xiàn): flow 構(gòu)造器,或 BroadcastChannel 接口,這兩種實現(xiàn)應(yīng)用于不同的場景。
1. 什么時候使用 Flow ?
Flow 是一種 "冷流"(Cold Stream)。"冷流" 是一種數(shù)據(jù)源,該類數(shù)據(jù)源的生產(chǎn)者會在每個監(jiān)聽者開始消費事件的時候執(zhí)行,從而在每個訂閱上創(chuàng)建新的數(shù)據(jù)流。一旦消費者停止監(jiān)聽或者生產(chǎn)者的阻塞結(jié)束,數(shù)據(jù)流將會被自動關(guān)閉。
Flow 非常適合需要開始/停止數(shù)據(jù)的產(chǎn)生來匹配觀察者的場景.
您可以利用 flow 構(gòu)造器來發(fā)送有限個/無限個元素。
val oneElementFlow: Flow<Int> = flow {
// 生產(chǎn)者代碼開始執(zhí)行,流被打開
emit(1)
// 生產(chǎn)者代碼結(jié)束,流將被關(guān)閉
}
val unlimitedElementFlow: Flow<Int> = flow {
// 生產(chǎn)者代碼開始執(zhí)行,流被打開
while(true) {
// 執(zhí)行計算
emit(result)
delay(100)
}
// 生產(chǎn)者代碼結(jié)束,流將被關(guān)閉
}
Flow 通過協(xié)程取消功能提供自動清理功能,因此傾向于執(zhí)行一些重型任務(wù)。請注意,這里提到的取消是有條件的,一個永不掛起的 Flow 是永不會被取消的: 在我們的例子中,由于 delay 是一個掛起函數(shù),用于檢查取消狀態(tài),當訂閱者停止監(jiān)聽時,F(xiàn)low 將會停止并清理資源。
2. 什么時候使用 BroadcastChannel
Channel 是一個用于協(xié)程間通信的并發(fā)原語。BroadcastChannel 基于 Channel,并加入了多播功能。
可能在這樣一些場景里,您可能會考慮在數(shù)據(jù)源層中使用 BroadcastChannel:
如果生產(chǎn)者和消費者的生命周期不同或者彼此完全獨立運行時,請使用 BroadcastChannel。
如果您希望生產(chǎn)者有獨立的生命周期,同時向任何存在的監(jiān)聽者發(fā)送當前數(shù)據(jù)的時候,BroadcastChannel API 非常適合這種場景。在這種情況下,當新的監(jiān)聽者開始消費事件時,生產(chǎn)者不需要每次都被執(zhí)行。
您依然可以向調(diào)用者提供 Flow,它們不需要知道具體的實現(xiàn)。您可以使用 BroadcastChannel.asFlow() 這個擴展函數(shù)來將一個 BroadcastChannel 作為一個 Flow 使用。
不過,關(guān)閉這個特殊的 Flow 不會取消訂閱。當使用 BroadcastChannel 的時候,您必須自己管理生命周期。BroadcastChannel 無法感知到當前是否還存在監(jiān)聽者,除非關(guān)閉或取消 BroadcastChannel,否則將會一直持有資源。請確保在不需要 BroadcastChannel 的時候?qū)⑵潢P(guān)閉。同時請注意關(guān)閉后的 BroadcastChannel 無法再次被使用,如果需要,您需要重新創(chuàng)建實例。
3. 特別說明
部分 Flow 和 Channel API 仍處于實驗階段,很可能會發(fā)生變動。在一些情況下,您可能會正在使用 Channel,不過在未來可能會建議您使用 Flow。具體來講,StateFlow 和 Flow 的 share operator 方案可能在未來會減少 Channel 的使用。
將數(shù)據(jù)流中基于回調(diào)的 API 轉(zhuǎn)化為協(xié)程
包含 Room 在內(nèi)的很多庫已經(jīng)支持將協(xié)程用于數(shù)據(jù)流操作。對于那些還不支持的庫,您可以將任何基于回調(diào)的 API 轉(zhuǎn)換為協(xié)程。
1. Flow 的實現(xiàn)
如果您想將一個基于回調(diào)的流 API 轉(zhuǎn)換為使用 Flow,您可以使用 channelFlow 函數(shù) (當然也可以使用 callbackFlow,它們都基于相同的實現(xiàn))。channelFlow 將會創(chuàng)建一個 Flow 的實例,該實例中的元素將傳遞給一個 Channel。這樣可以允許我們在不同的上下文或并發(fā)中提供元素。
以下示例中,我們想要把從回調(diào)中拿到的元素發(fā)送到 Flow 中:
- 利用 channelFlow 構(gòu)造器創(chuàng)建一個可以把回調(diào)注冊到第三方庫的流;
- 將從回調(diào)接收到的所有數(shù)據(jù)傳遞給 Flow;
- 當訂閱者停止監(jiān)聽,我們利用掛起函數(shù) "awaitClose" 來解除 API 的訂閱。
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
override fun getObservableUserEvent(userId: String, eventId: SessionId): Flow<UserEventResult> {
// 1) 利用 channelFlow 創(chuàng)建一個 Flow
return channelFlow<UserEventResult> {
val eventDocument = firestore.collection(USERS_COLLECTION)
.document(userId)
.collection(EVENTS_COLLECTION)
.document(eventId)
// 1) 將回調(diào)注冊到 API 上
val subscription = eventDocument.addSnapshotListener { snapshot, _ ->
val userEvent = if (snapshot.exists()) {
parseUserEvent(snapshot)
} else { null }
// 2) 將數(shù)據(jù)發(fā)送到 Flow
channel.offer(UserEventResult(userEvent))
}
// 3) 請不要關(guān)閉數(shù)據(jù)流,在消費者關(guān)閉或者 API 調(diào)用 onCompleted/onError 函數(shù)之前,請保證數(shù)據(jù)流
// 一直處于打開狀態(tài)。
// 當數(shù)據(jù)流關(guān)閉后,請取消第三方庫的訂閱。
awaitClose { subscription.remove() }
}
}
2. BroadcastChannel 實現(xiàn)
對于使用 Firestore 跟蹤用戶身份認證的數(shù)據(jù)流,我們使用了 BroadcastChannel API,因為我們希望注冊一個有獨立生命周期的 Authentication 監(jiān)聽者,同時也希望能向所有正在監(jiān)聽的對象廣播當前的結(jié)果。
轉(zhuǎn)化回調(diào) API 為 BroadcastChannel 相比轉(zhuǎn)化為 Flow 要略復(fù)雜一點。您可以創(chuàng)建一個類,并設(shè)置將實例化后的 BroadcastChannel 作為變量保存。在初始化期間,注冊回調(diào),像以前一樣將元素發(fā)送到 BroadcastChannel:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
class FirebaseAuthStateUserDataSource(...) : AuthStateUserDataSource {
private val channel = ConflatedBroadcastChannel<Result<AuthenticatedUserInfo>>()
private val listener: ((FirebaseAuth) -> Unit) = { auth ->
// 數(shù)據(jù)處理邏輯
// 將當前的用戶 (數(shù)據(jù)) 發(fā)送給消費者
if (!channel.isClosedForSend) {
channel.offer(Success(FirebaseUserInfo(auth.currentUser)))
} else {
unregisterListener()
}
}
@Synchronized
override fun getBasicUserInfo(): Flow<Result<AuthenticatedUserInfo>> {
if (!isListening) {
firebase.addAuthStateListener(listener)
isListening = true
}
return channel.asFlow()
}
}
測試小建議
為了測試 Flow 轉(zhuǎn)換 (就像我們在 UseCase 和 Repository 層中所做的那樣),您可以利用 flow 構(gòu)造器返回一個假數(shù)據(jù),例如:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
object FakeUserEventDataSource : UserEventDataSource {
override fun getObservableUserEvents(userId: String) = flow {
emit(UserEventsResult(userEvents))
}
}
class DefaultSessionAndUserEventRepositoryTest {
@Test
fun observableUserEvents_areMappedCorrectly() = runBlockingTest {
// 準備一個 repo
val userEvents = repository
.getObservableUserEvents("user", true).first()
// 對接收到的用戶事件進行斷言
}
}
為了成功完成測試,一個比較好的做法是使用 take 操作符來從 Flow 中獲取一些數(shù)據(jù),使用 toList 作為末端操作符來從數(shù)組中獲取結(jié)果。示例如下:
class AnotherStreamDataSourceImplTest {
@Test
fun `Test happy path`() = runBlockingTest {
//準備好 subject
val result = subject.flow.take(1).toList()
// 斷言結(jié)果和預(yù)期的一致
}
}
take 操作符非常適合在獲取到數(shù)據(jù)后關(guān)閉 Flow。在測試完畢后不關(guān)閉 Flow 或 BroadcastChannel 將會導(dǎo)致內(nèi)存泄漏以及測試結(jié)果不一致。
注意: 如果在數(shù)據(jù)源的實現(xiàn)是通過 BroadcastChannel 完成的,那么上面的代碼還不夠。您需要自己管理數(shù)據(jù)源的生命周期,并確保 BroadcastChannel 在測試開始之前已經(jīng)啟動,同時需要在測試結(jié)束后將其關(guān)閉,否則將會導(dǎo)致內(nèi)存泄漏。
協(xié)程測試的最佳實踐在這里依然適用。如果您在測試代碼中創(chuàng)建新的協(xié)程,則可能想要在測試線程中執(zhí)行它來確保測試獲得執(zhí)行。
您也可以通過視頻回顧 2019 Android 開發(fā)者峰會演講 —— 在 Android 上測試協(xié)程:.
騰訊視頻鏈接: https://v.qq.com/x/page/d3037jjhkh3.html
總結(jié)
- 因為 Flow 所提供的更加明確的約束和各種操作符,我們更建議向消費者暴露 Flow 而不是 Channel;
- 使用 Flow 時,生產(chǎn)者會在每次有新的監(jiān)聽者時被執(zhí)行,同時數(shù)據(jù)流的生命周期將會被自動處理;
- 使用 BroadcastChannel 時,您可以共享生產(chǎn)者,但需要自己管理它的生命周期;
- 請考慮將基于回調(diào)的 API 轉(zhuǎn)化為協(xié)程,以便在您的應(yīng)用中更好、更慣用地集成 API;
- 使用 take 和 toList 操作符可以簡化 Flow 的相關(guān)代碼測試。
2019 ADS 應(yīng)用在 GitHub 開源,請訪問下方鏈接在 GitHub 上查看更詳細的代碼實現(xiàn): https://github.com/google/iosched/tree/adssched