Android随笔-kotlin Flow

Kotlin Flow 是 Kotlin 协程生态中的响应式数据流(Reactive Stream)实现 ,用于异步、顺序、可组合地发射多个值。你可以把它理解为"基于协程的 RxJava 轻量替代",但设计理念更贴合 Kotlin 的挂起函数和结构化并发。

一、核心概念:Flow 到底是什么

1. 定义

kotlin 复制代码
// Flow 的接口定义极其简单
public interface Flow<out T> {
    public abstract suspend fun collect(collector: FlowCollector<T>)
}

本质 : Flow 就是一个挂起函数 collect,接收一个 FlowCollector 回调。每次调用 collect,内部的代码块顺序执行, emit 一个值就回调一次。

2. 冷流(Cold Stream)

Flow 是冷流:不消费就不生产,每次消费都从头生产。

kotlin 复制代码
val flow = flow {
    println("开始生产")
    emit(1)
    emit(2)
}

// 第一次收集
flow.collect { println("收到: $it") }  // 输出:开始生产 / 收到: 1 / 收到: 2

// 第二次收集
flow.collect { println("收到: $it") }  // 再次输出:开始生产 / 收到: 1 / 收到: 2

对比热流(Hot Stream,如 StateFlow):不管有没有观察者,都在生产数据。

3. 与 RxJava 的核心区别

特性 Kotlin Flow RxJava
基础 基于 Kotlin 协程/挂起函数 基于 JVM 线程/回调
线程切换 flowOn(Dispatchers.IO),简单直观 subscribeOn/observeOn 组合复杂
背压 默认挂起(背压天然友好) 多种策略(BUFFER、DROP、LATEST 等)
异常处理 catch 操作符 + try/catch onError 回调
生命周期 天然支持结构化并发(自动取消) 需手动管理 Disposable
学习成本 低(挂起函数直觉) 高(大量操作符和线程概念)
包体积 小(协程核心 + Flow) 大(RxJava 全家桶)

4. 核心操作符

kotlin 复制代码
flowOf(1, 2, 3, 4, 5)
    .filter { it > 2 }           // 过滤
    .map { it * 2 }              // 变换
    .flowOn(Dispatchers.IO)      // 上游在 IO 线程执行
    .catch { emit(-1) }          // 异常捕获
    .onEach { println(it) }      // 副作用
    .collect()                   // 终端操作符(触发执行)

二、常见问题

Q1:Flow 是什么?为什么要用 Flow 而不是 LiveData/RxJava?

  • Flow 是 Kotlin 协程提供的冷流(Cold Stream) API,用于处理异步顺序发射多个值的场景。
  • 相比 LiveData:
    • Flow 支持丰富操作符(map、filter、debounce 等),LiveData 的 Transformations 能力有限。
    • Flow 支持线程切换(flowOn),LiveData 只能在主线程消费。
    • Flow 是 Kotlin 多平台通用,LiveData 是 Android 专属。
    • Flow 支持背压,LiveData 没有背压概念(setValue 太快会丢值)。
  • 相比 RxJava:
    • Flow 基于挂起函数,代码更线性、可读性更高,避免回调地狱。
    • Flow 天然支持结构化并发(viewModelScope.launch 取消时自动取消 Flow 收集)。
    • 包体积更小,无需引入 RxJava 全家桶。

Q2:什么是冷流(Cold Flow)和热流(Hot Flow)?Flow 属于哪种?

  • 冷流: 不收集就不生产数据,每次收集都独立从头执行。Flow 是冷流。
  • 热流: 生产者独立存在,不管有没有收集者都在生产数据。StateFlow、SharedFlow 是热流。
kotlin 复制代码
// 冷流:每次 collect 都重新执行 flow 块
val coldFlow = flow { emit(1); emit(2) }

// 热流:有独立的生产者,多个收集者共享同一数据
val hotFlow = MutableStateFlow(0)

冷流转热流: 使用 stateIn 或 shareIn 操作符。

Q3:StateFlow、SharedFlow 和 LiveData 的区别?

特性 StateFlow SharedFlow LiveData
类型 热流(有初始值) 热流(无初始值) 可观察数据持有者
初始值 必须有 可以没有 可以没有
重播 始终重播最新值(replay=1 可配置 replay 数量 始终重播最新值
背压 onBufferOverflow 策略 onBufferOverflow 策略 无,直接覆盖
观察者 需要 LifecycleScope/repeatOnLifecycle 同上 自动感知生命周期
线程 任意线程收集,通常 flowOn 切换 同上 主线程观察
适用 UI 状态(如 Loading/Success/Error) 事件(如 Toast、Navigation) 简单 UI 状态

关键区分

  • StateFlow 适合状态(State):有初始值,新观察者总能拿到最新状态。
  • SharedFlow 适合事件(Event):如 Channel 的替代,配置 replay=0 时新观察者收不到历史事件。
  • LiveData 在纯 Kotlin 项目中被 StateFlow 替代趋势明显,但在 Java 项目中仍广泛使用。

Q4:flowOn 的作用是什么?它切换的是上游还是下游的线程?

  • flowOn 改变的是上游操作符的执行线程(上下文)。
  • 下游操作符和终端操作符(如 collect)运行在调用者(收集者)的协程上下文中。
kotlin 复制代码
flow {
    // 运行在 Dispatchers.IO
    emit(fetchData())  
}.map { 
    // 运行在 Dispatchers.IO(上游)
    it.process() 
}.flowOn(Dispatchers.IO)
 .filter {
    // 运行在 collect 所在的线程(默认主线程)
    it.isValid() 
}.collect {
    // 运行在 collect 所在的线程(默认主线程)
    updateUI(it) 
}

原理 : flowOn 会在上游和下游之间插入一个通道 (Channel),上游在指定调度器生产,下游在收集者调度器消费,实现生产者-消费者解耦。

Q5:Flow 的背压(Backpressure)是如何处理的?

  • Flow 基于挂起函数的特性,天然对背压友好:当消费者处理慢时,生产者(emit)会挂起等待,不会丢数据。
  • 对比 RxJava 需要显式选择策略(BUFFER、DROP、LATEST),Flow 默认行为就是挂起缓冲。
  • 如果希望不挂起,可以使用 buffer()、conflate()、collectLatest() 等操作符主动控制:
kotlin 复制代码
flow {
    emit(1); emit(2); emit(3)
}.buffer(10)          // 缓冲 10 个元素,emit 不挂起
 .conflate()         // 只保留最新值,中间值丢弃
 .collectLatest {    // 有新值时取消上一个值的收集
     delay(100)      // 处理慢
 }

Q6:Flow 的异常处理机制有哪些?catch 能捕获哪些异常?

Flow 异常处理有三种方式:

1. try/catch 包裹 collect:
kotlin 复制代码
try {
    flow.collect { ... }
} catch (e: Exception) { ... }

可以捕获上游和下游(collect 块内)的异常。

2. catch 操作符:
kotlin 复制代码
flow { ... }
    .map { ... }
    .catch { e -> 
        emit(defaultValue)  // 可以发射默认值
    }
    .collect()
  • catch 只捕获上游异常(catch 之前的操作符)。
  • 不能捕获 collect 块内的异常(下游)。
  • catch 内可以再次 emit,实现异常恢复。
3. onCompletion:
kotlin 复制代码
.onCompletion { cause -> 
    // cause 为 null 表示正常完成,非 null 表示异常取消
}

类似 finally,无论成功、失败还是取消都会执行。

重要: catch 不会捕获 CancellationException(协程取消是正常流程,不是异常)。

Q7:collect、collectLatest、launchIn 有什么区别?

终端操作符 行为 适用场景
collect 挂起式顺序收集,处理完一个才收下一个 需要处理每一个值
collectLatest 收到新值时,取消上一个值的收集协程 搜索输入防抖、位置更新(只关心最新)
launchIn(scope) 在指定 Scope 内启动收集,非阻塞 需要同时启动多个 Flow,或绑定生命周期
first() / single() 取第一个/唯一一个值,然后取消 一次性查询
kotlin 复制代码
// collectLatest:用户快速输入 "abc",只处理 "c" 的搜索
searchFlow
    .debounce(300)
    .flatMapLatest { query -> search(query) }
    .collectLatest { result -> updateUI(result) }

// launchIn:在 viewModelScope 中启动收集,返回 Job 可取消
flow.launchIn(viewModelScope)

Q8:如何在 Android 中安全地收集 Flow?repeatOnLifecycle 的作用?

在 Activity/Fragment 中直接 lifecycleScope.launch { flow.collect() } 会导致后台应用仍收集数据,浪费资源甚至崩溃。

正确做法:

kotlin 复制代码
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUI(state)
        }
    }
}
  • repeatOnLifecycle 在生命周期进入 STARTED 时启动收集,低于 STARTED 时自动取消并重新订阅。
  • 保证 UI 不可见时不收集,避免内存泄漏和无效更新。
  • 适用于 StateFlow 等热流。

替代方案:

  • flowWithLifecycle(flow, Lifecycle.State.STARTED):将 Flow 转换为生命周期感知的 Flow。
  • LifecycleOwner.addRepeatingJob(Kotlin 1.6+ 扩展)。

Q9:flatMapConcat、flatMapMerge、flatMapLatest 的区别?

操作符 行为 类比 RxJava
flatMapConcat 顺序处理:上一个内部 Flow 完成后才启动下一个 concatMap
flatMapMerge 并发处理:同时启动多个内部 Flow,结果按完成顺序合并 flatMap
flatMapLatest 收到新值时,取消上一个未完成的内部 Flow switchMap
kotlin 复制代码
flowOf(1, 2, 3)
    .flatMapLatest { value ->
        flow {
            delay(100)
            emit(value)
        }
    }
    .collect { println(it) }  // 快速发射时,只保留最新值

Q10:Flow 在 MVVM 架构中的典型使用方式?

ViewModel 层:

kotlin 复制代码
class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events.asSharedFlow()

    fun loadData() {
        viewModelScope.launch {
            repository.fetchData()
                .onStart { _uiState.value = UiState.Loading }
                .catch { e -> _uiState.value = UiState.Error(e.message) }
                .collect { data ->
                    _uiState.value = UiState.Success(data)
                }
        }
    }
}

UI 层(Fragment):

kotlin 复制代码
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch { viewModel.uiState.collect { updateUI(it) } }
        launch { viewModel.events.collect { handleEvent(it) } }
    }
}

分层原则

  • StateFlow 暴露状态(UI 的完整数据快照)。
  • SharedFlow 暴露事件(一次性通知,如导航、Toast)。
  • 业务逻辑中的数据流用冷 Flow,在 ViewModel 中通过 stateIn 转为热流供 UI 订阅。

三、总结

问题 一句话答案
Flow 是什么 基于协程的冷流,顺序异步发射多个值
冷流 vs 热流 冷流按需生产(Flow),热流持续生产(StateFlow/SharedFlow)
StateFlow vs SharedFlow StateFlow 有初始值重播最新状态;SharedFlow 配置灵活,适合事件
flowOn 切换上游操作符的调度器,下游运行在 collect 所在线程
背压 默认挂起等待;可用 buffer/conflate/collectLatest 主动控制
异常处理 catch 捕获上游;try/catch 包裹 collect 捕获全部;onCompletion 类似 finally
生命周期收集 用 repeatOnLifecycle(Lifecycle.State.STARTED) 避免后台收集
终端操作符 collect 顺序处理;collectLatest 取消旧值;launchIn 非阻塞启动
相关推荐
阳光予你1 小时前
App 启动流程:从 Launcher 点击到 Activity 创建
android
小孔龙2 小时前
BufferQueue 对象交接与同步
android
XiaoLeisj2 小时前
Kotlin Flow 常用操作符:数据变换 map、filter、onEach,时间控制 debounce、sample,终端聚合 reduce、fold
android·kotlin·android jetpack·协程·响应式编程·flow
EQ-雪梨蛋花汤2 小时前
Android 3D 开发教程(三):Sceneform-EQR 配置 PBR 材质、光照、相机与实时阴影
android·3d·材质
Dovis(誓平步青云)3 小时前
《 固井工程软件 Cemsol 的数据管理与国产化适配实践》
android·java·开发语言·人工智能
Kapaseker3 小时前
小白都看得懂的 Skill 教程 - 创建第一个 Skill
android·kotlin·vibecoding
zhangphil3 小时前
Android BitmapFactory实现AOSP ContentResolver.loadThumbnail快速取小缩略图,Kotlin
android·kotlin
weixin_440784113 小时前
【OkHttp实现原理】
android·java·okhttp
恋猫de小郭3 小时前
Jetpack Compose 8 月版正式发布,核心模块 1.12
android·前端·flutter