Kotlin Flow 中 flatMap 与 flatMapLatest 的核心差异 —— 新手指南

0 阅读4分钟

1. 差异

flatMap

  • 行为:转换每个输入值到 Flow,并按顺序收集所有生成的 Flow
  • 特点:新的输入不会取消之前正在进行的转换
  • 使用场景:需要处理所有事件,且事件间有依赖关系或需保持顺序

flatMapLatest

  • 行为:当有新输入时,立即取消前一个转换的 Flow
  • 特点:只处理最新的输入,忽略中间结果
  • 使用场景:只需最新结果,可安全取消旧操作
// 对比示例
fun demonstrateDifference() {
    runBlocking {
        val flow = flowOf(1, 2, 3).onEach { delay(100) }
        
        // flatMap:处理所有值
        flow.flatMap { value ->
            flow {
                emit("Processing $value")
                delay(200) // 模拟耗时操作
                emit("Completed $value")
            }
        }.collect { println("flatMap: $it") }
        // 输出所有 1,2,3 的处理结果
        
        // flatMapLatest:只处理最新的
        flow.flatMapLatest { value ->
            flow {
                emit("Latest: $value")
                delay(200)
                emit("Done: $value") // 可能被取消
            }
        }.collect { println("flatMapLatest: $it") }
        // 只输出 3 的最新结果
    }
}

2. 实战场景分析

场景一:搜索自动补全

class SearchViewModel {
    private val searchQuery = MutableStateFlow("")
    
    // 使用 flatMapLatest:用户连续输入时取消之前的请求
    val searchResults = searchQuery
        .debounce(300) // 防抖
        .filter { it.length >= 2 }
        .flatMapLatest { query ->
            flow {
                emit(SearchState.Loading)
                try {
                    val results = api.searchAutocomplete(query)
                    emit(SearchState.Success(results))
                } catch (e: Exception) {
                    emit(SearchState.Error(e))
                }
            }
        }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), SearchState.Idle)
    
    fun onQueryChanged(query: String) {
        searchQuery.value = query
    }
}

// 错误使用 flatMap 的情况:所有请求都会完成,可能导致结果显示错乱
val wrongResults = searchQuery
    .flatMap { query -> // 错误!应该用 flatMapLatest
        api.searchAutocompleteFlow(query)
    }

场景二:位置更新

class LocationTracker {
    private val locationUpdates = locationProvider.getUpdates()
    
    // 使用 flatMap:每个位置都要上传,顺序重要
    val uploadStatus = locationUpdates
        .filter { it.accuracy < 50 } // 只处理高精度位置
        .conflate() // 合并位置更新,避免过快
        .flatMap { location ->
            flow {
                try {
                    val response = uploadToServer(location)
                    emit(UploadResult.Success(location.id, response))
                } catch (e: Exception) {
                    emit(UploadResult.Failure(location.id, e))
                }
            }
        }
        .catch { e -> emit(UploadResult.NetworkError(e)) }
    
    // 使用 flatMapLatest:只关心最新位置的周边搜索
    val nearbyPlaces = locationUpdates
        .flatMapLatest { location ->
            placesApi.getNearbyPlaces(location.lat, location.lng)
        }
}

场景三:文件上传队列

class FileUploadManager {
    private val uploadQueue = MutableSharedFlow<FileData>()
    
    // 使用 flatMap + buffer:并发上传但限制并发数
    val uploadProgress = uploadQueue
        .flatMapMerge(concurrency = 3) { file -> // 限制3个并发
            uploadFileFlow(file)
        }
        .shareIn(ioScope, SharingStarted.Lazily)
    
    private fun uploadFileFlow(file: FileData): Flow<UploadStatus> = flow {
        emit(UploadStatus.Progress(file.id, 0))
        
        val chunks = splitIntoChunks(file)
        chunks.forEachIndexed { index, chunk ->
            api.uploadChunk(file.id, chunk)
            val progress = ((index + 1) / chunks.size.toFloat() * 100).toInt()
            emit(UploadStatus.Progress(file.id, progress))
        }
        
        emit(UploadStatus.Completed(file.id))
    }
    
    // 使用 flatMapLatest:用户取消上传时立即停止
    fun uploadWithCancel(): Flow<UploadStatus> {
        val cancelSignal = MutableStateFlow(false)
        
        return uploadQueue
            .flatMapLatest { file ->
                if (cancelSignal.value) {
                    emptyFlow()
                } else {
                    uploadFileFlow(file)
                }
            }
    }
}

3. 常见陷阱与解决方案

陷阱一:资源泄漏

// 错误:flatMapLatest 取消时资源未清理
flow.flatMapLatest { id ->
    flow {
        val connection = openDatabaseConnection() // 可能泄漏!
        try {
            val data = connection.query(id)
            emit(data)
        } finally {
            connection.close() // 必须清理
        }
    }
}

// 正确:使用 cancellable 操作或清理资源
flow.flatMapLatest { id ->
    callbackFlow {
        val connection = openDatabaseConnection()
        try {
            val data = connection.query(id)
            send(data)
            awaitClose()
        } finally {
            connection.close()
        }
    }
}

陷阱二:状态不一致

// 错误:状态更新可能被取消,导致不一致
var currentState = State.IDLE

flow.flatMapLatest {
    currentState = State.LOADING // 可能执行但后续被取消
    apiCallFlow(it).onCompletion {
        currentState = State.IDLE // 可能不会执行
    }
}

// 正确:使用状态流
val state = MutableStateFlow(State.IDLE)

flow.flatMapLatest {
    state.value = State.LOADING
    apiCallFlow(it)
        .catch { e -> 
            state.value = State.ERROR(e)
            emptyFlow() 
        }
        .onCompletion { 
            if (it == null) state.value = State.IDLE 
        }
}

陷阱三:背压处理不当

// 错误:没有处理背压,可能内存溢出
highFrequencyFlow
    .flatMapLatest { // 仍然可能快速发射
        heavyOperationFlow(it)
    }

// 正确:添加背压策略
highFrequencyFlow
    .conflate() // 合并中间值
    .flatMapLatest {
        heavyOperationFlow(it)
    }

// 或限制并发
highFrequencyFlow
    .flatMapMerge(concurrency = 1) { // 限制为顺序执行
        heavyOperationFlow(it)
    }

陷阱四:异常处理缺失

// 错误:异常会使整个流终止
flow.flatMapLatest {
    flow { 
        if (it.isEmpty()) throw IllegalArgumentException()
        emit(process(it))
    }
}

// 正确:妥善处理异常
flow.flatMapLatest {
    flow {
        try {
            if (it.isEmpty()) throw IllegalArgumentException()
            emit(Result.Success(process(it)))
        } catch (e: Exception) {
            emit(Result.Failure(e))
        }
    }
}

4. 性能优化技巧

// 1. 合并操作减少开销
searchQuery
    .debounce(300)
    .distinctUntilChanged() // 避免重复查询
    .flatMapLatest { query ->
        api.search(query)
            .retry(2) // 重试机制
            .timeout(5000) // 超时
    }

// 2. 使用 shareIn 避免重复订阅
val sharedFlow = sourceFlow
    .flatMapLatest { expensiveOperation(it) }
    .shareIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        replay = 1
    )

// 3. 适当使用 buffer
flow
    .buffer(Channel.UNLIMITED) // 根据场景选择
    .flatMapLatest { /* ... */ }

5. 选择指南

场景特征推荐操作符理由
需处理所有输入,顺序重要flatMapConcat保持顺序,无并发
需处理所有输入,顺序不重要flatMapMerge并发处理,提高效率
只需最新结果,可取消旧操作flatMapLatest避免无效计算
有限并发控制flatMapMerge with concurrency控制资源使用
冷流转换transform更轻量级的转换

总结

  1. flatMapLatest 适合响应式 UI 交互(搜索、点击防抖),可避免陈旧数据
  2. flatMapMerge 适合并行任务处理(批量上传、并发请求),提高吞吐量
  3. flatMapConcat 适合顺序敏感操作(文件分片上传、数据库事务)
  4. 始终考虑资源清理,特别是在可取消的操作中
  5. 合理处理背压,根据场景选择 buffer、conflate 等策略
  6. 统一异常处理,避免流意外终止

正确选择 flatMap 变体可以显著提升应用性能和用户体验,特别是在处理异步数据流时。