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

1. 差异

flatMap

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

flatMapLatest

  • 行为 :当有新输入时,立即取消前一个转换的 Flow
  • 特点:只处理最新的输入,忽略中间结果
  • 使用场景:只需最新结果,可安全取消旧操作
kotlin 复制代码
// 对比示例
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. 实战场景分析

场景一:搜索自动补全

kotlin 复制代码
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)
    }

场景二:位置更新

kotlin 复制代码
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)
        }
}

场景三:文件上传队列

kotlin 复制代码
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. 常见陷阱与解决方案

陷阱一:资源泄漏

kotlin 复制代码
// 错误: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()
        }
    }
}

陷阱二:状态不一致

kotlin 复制代码
// 错误:状态更新可能被取消,导致不一致
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 
        }
}

陷阱三:背压处理不当

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

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

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

陷阱四:异常处理缺失

kotlin 复制代码
// 错误:异常会使整个流终止
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. 性能优化技巧

kotlin 复制代码
// 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 变体可以显著提升应用性能和用户体验,特别是在处理异步数据流时。

相关推荐
恋猫de小郭19 分钟前
Flutter 全新真 3D 实现,用 flutter_scene 能开发一个「我的世界」
android·前端·flutter
HLC++13 小时前
Linux的进程间通信
android·linux·服务器
爱笑鱼17 小时前
Binder(二):AIDL 生成的 Proxy、Stub 和 Parcel 到底在做什么?
android
爱笑鱼17 小时前
Binder(一):一次方法调用,究竟怎样跨进程执行?
android
btown18 小时前
Kotlin 版 MyBatis-Plus 查询优雅解决方案
kotlin
壮哥_icon18 小时前
【Android 系统开发】使用 BAT 脚本高效自动化管理 /system/priv-app/ 系统应用(安装与卸载)
android·运维·自动化
用户693717500138419 小时前
Claude Code终端日志Token占用实测:一个过滤器砍掉60%-90%
android·前端·后端
蝉蜕日记20 小时前
AccumuPDF高级版 v2.57 | 视图文转换PDF,合并分割压缩
android·智能手机·pdf·生活·软件需求
吐了啊取名字太难20 小时前
美颜系统AI修图本地跑并支持Mac、win、安卓、iOS不卡顿
android·人工智能·windows·数码相机·mac·ai编程
小二·21 小时前
车载数字座舱实战:用魔珐星云打造下一代智能驾乘助手(附 Android/Kotlin 完整代码)
开发语言·kotlin