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

相关推荐
alexhilton2 小时前
Compose中的ContentScale:终极可视化指南
android·kotlin·android jetpack
jzlhll1232 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
Digitally4 小时前
2026 年 8 款安卓数据擦除软件和应用对比
android
杨忆4 小时前
android 11以上 截图工具类
android
粤M温同学4 小时前
Android Studio 中安装 CodeBuddy AI助手
android·ide·android studio
阿拉斯攀登5 小时前
【RK3576 安卓 JNI/NDK 系列 08】RK3576 实战(二):JNI 调用 I2C 驱动读取传感器数据
android·安卓ndk入门·jni方法签名·java调用c++·rk3576底层开发·rk3576 i2c开发
阿巴斯甜5 小时前
Compose中CompositionLocal 的使用
android jetpack
赶路人儿7 小时前
常见的mcp配置
android·adb
阿巴斯甜7 小时前
Compose中 MutableState的状态区别:
android jetpack
符哥20087 小时前
充电桩 WiFi 局域网配网(Android/Kotlin)流程、指令及实例说明文档
android·开发语言·kotlin