ViewModel負責組裝界面狀態State。引發State變換的原因有很多,比如用戶點擊某個按鈕,一次網絡請求受到應答,一次本地數據庫查詢返回結果等等。因此ViewModel是根據各種事件生成State的對象,換句話說,是一個從多個事件流到狀態的映射。
ViewModel: (InputFlow, NetworkFlow, DatabaseFlow, ...) -> State
用圖來表示就是
┌─────────┐ 用戶交互 ───? 事件流 ───┤ ││ViewModel├───? UI 狀態 ───? 渲染UI 數據變更 ───? 數據流 ───┤ │└─────────┘
1.?常見事件流處理模式
我們考慮處理兩個事件流的模式。多事件流的處理方法是類似的。
1.1.?模式1:取各自最新值
combine(flowA, flowB) { a, b ->// 當某個流更新時,獲取兩個流各自的最新值。Result(a, b) }
適用場景:表單聯動驗證(如郵箱+密碼驗證)、實時數據看板。
1.2.?模式2:合并流
merge(flowA, flowB) // 任意流更新時觸發,取兩個流中的最新值。
適用場景:同類型事件合并(如多個按鈕點擊事件)
1.3.?模式3:流水線
flowA.flatMapLatest { aValue -> flowB.map { bValue -> Processed(aValue, bValue) } }
適用場景:搜索建議(關鍵詞變化觸發新查詢)、參數化數據加載
1.4.?模式4:序號匹配
flowA.zip(flowB) { a, b -> // 嚴格按序號匹配,雙方各發1次才觸發。Pair(a, b) }
適用場景:分頁加載、操作-響應確認機制。
1.5.?模式5:優先流
combine(flowA, flowB) { a, b ->when {a.priority > b.priority -> Result(a)b.isCritical -> Result(b)else -> Result.merge(a, b)} }
特點:實現業務規則主導的數據優先級。 適用場景:多源數據沖突處理、關鍵操作優先。
1.6.?模式6:將異常轉換為事件
val flowAState = flowA.map { Success(it) }.catch { emit(Error(it)) }.stateIn(viewModelScope, SharingStarted.Lazily, Loading)
特點:單個流的失敗不影響整體功能。 適用場景:模塊化數據展示、獨立可失敗操作。
2.?常見問題和方案
2.1.?問題1:輸入流過多
輸入流過多會導致代碼可讀性下降。封裝中間事件和中間流可以解決這個問題。
// 創建中間組合流 val userPreferences = combine(themeFlow, fontSizeFlow, layoutFlow) { UserPrefs(it[0], it[1], it[2]) }val finalState = combine(userPrefs, contentFlow) { ... }
2.2.?問題2:狀態頻繁更新導致UI抖動
在流中增加防抖可以避免UI抖動。
searchQueryFlow.debounce(300) // 防抖.distinctUntilChanged().flatMapLatest { query -> repository.search(query).map { it.toState() } }
2.3.?問題3:訂閱泄漏
使用stateIn和flatMapLatest可以避免訂閱泄露。
代碼1??錯誤例子:
init {viewModelScope.launch { // 直接啟動協程dataFlow.collect { ... }} }
代碼2??正確例子
val state = dataFlow.map { ... }.stateIn( // 使用stateIn擴展scope = viewModelScope,started = SharingStarted.Eagerly,initialValue = ...)
2.4.?問題4:錯誤恢復
使用retryWhen可以從錯誤中進行恢復。
flowA.retryWhen { cause, attempt -> if (attempt < 3) delay(200 * attempt) else throw cause}.catch { emit(fallbackData) }
2.5.?問題5:動態切換流
val activeFlow = MutableStateFlow<Flow<Data>>(flowA)val result = activeFlow.flatMapLatest { it }
支持在運行時切換數據源(如測試模式/生產模式切換)。
2.6.?問題6:緩存共享流
復用單數據源,減少重復訂閱。
// 創建共享流 val sharedFlow = repository.dataSource.shareIn(viewModelScope, SharingStarted.WhileSubscribed())// 分流處理 val stateA = sharedFlow.map { extractA(it) } val stateB = sharedFlow.map { extractB(it) }
2.7.?問題7:將業務狀態作為界面狀態
不要直接使用業務狀態作為界面狀態(業務狀態極為簡單的除外)。
// 錯誤方式:混合業務狀態和UI控制狀態 data class BadState(val data: List<Item>,val loading: Boolean,val toastMessage: String?, // UI控制狀態val dialogVisible: Boolean // UI控制狀態 )// 正確方式:分層狀態 // 業務狀態 data class BusinessState(val data: List<Item>, val error: Throwable?)// UI狀態(由業務狀態轉換) val uiState = businessState.map { when {it.data.isEmpty() -> EmptyStateit.error != null -> ErrorState(it.error)else -> SuccessState(it.data)} }