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

相关推荐
DogDaoDao4 小时前
Android 硬件编码器参数完全指南:MediaCodec 深度解析
android·音视频·视频编解码·h264·硬编码·视频直播·mediacodec
JohnnyDeng945 小时前
Android 自定义 View:Canvas 绘图与事件分发深度解析
android
Android小码家8 小时前
Framework之Launcher小窗开发
android·framework·虚拟屏·小窗
赏金术士8 小时前
第七章:状态管理实战与架构总结
android·ui·kotlin·compose
颂love9 小时前
MySQL的执行流程
android·数据库·mysql
云起SAAS14 小时前
抖音小游戏源码 - 消消乐 | 含激励广告+成就系统 | 开箱即用商业级消除游戏模板
android·游戏·广告联盟·看激励广告联盟流量主·抖音小游戏源码 - 消消乐
大貔貅喝啤酒15 小时前
基于Windows下载安装Android Studio 3.3.2版本教程(2026详细图文版)
android·java·windows·android studio
程序员码歌15 小时前
OpenSpec 到 Superpowers:AI 编码从说清到做对
android·前端·人工智能
2501_9151063215 小时前
深入解析无源码iOS加固原理与方案,保护应用安全
android·安全·ios·小程序·uni-app·cocoa·iphone
黄林晴19 小时前
重磅官宣:Android UI 开发正式进入 Compose-first 时代
android·google io