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 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui