Kotlin Flow 流全面详解

402 阅读7分钟

Kotlin Flow 流全面详解

一、Flow 简介

1. 什么是 Flow

Flow(流)是 Kotlin 协程生态中用于处理异步数据流的组件,由 JetBrains 官方推出,旨在简化异步场景下的数据传递与处理逻辑。它具备以下核心特性:

  • 冷流特性:默认是 “冷” 的,即只有当存在终端操作(如collect)时,数据流才会开始发射(发送)数据;没有订阅者时,不会执行任何计算或数据生成操作。

  • 协程兼容:完全基于协程构建,支持挂起函数(suspend),可无缝结合协程的生命周期管理,避免回调地狱。

  • 数据流特性:支持数据的连续发射(如多次发送 int、String 等类型数据),并可通过操作符对数据进行过滤、转换、组合等处理。

2. Flow 与其他类似组件的区别

组件核心差异适用场景
Flow冷流、协程兼容、支持背压、数据流连续发射异步数据流处理(如网络请求 + 本地缓存、实时数据更新)
Sequence同步数据流、无协程支持、不支持挂起函数同步场景下的集合数据迭代
Channel热流、支持点对点通信、无背压(需手动处理)协程间的消息传递
LiveData热流、Android 生命周期感知、仅支持单次数据发射(数据覆盖)、无协程原生支持Android UI 状态更新(已逐步被 StateFlow 替代)

二、Flow 的基础结构

Flow 的核心操作分为三个阶段:

  1. 创建流(Flow Builder):通过 Flow 构建器生成数据流(如flowOfasFlowflow)。

  2. 中间操作(Intermediate Operators):对数据流进行处理(如过滤、转换、限流),返回新的 Flow,操作是 “惰性” 的(不触发数据发射)。

  3. 终端操作(Terminal Operators):触发数据流的发射与收集(如collecttoList),是 “饥饿” 的(触发整个流的执行)。

// Flow三阶段示例

import kotlinx.coroutines.delay

import kotlinx.coroutines.flow.*

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {

   // 1. 创建流(Flow Builder)

   val numberFlow = flow {

       for (i in 1..3) {

           delay(1000) // 模拟异步操作(如网络请求、数据库读取)

           emit(i) // 发射数据(suspend函数,可挂起)

       }

   }

   // 2. 中间操作(过滤偶数、转换为字符串)

   val processedFlow = numberFlow

       .filter { it % 2 == 1 } // 过滤奇数

       .map { "奇数: $it" }     // 转换为字符串

   // 3. 终端操作(触发收集)

   processedFlow.collect { value ->

       println("收集到数据:$value")

   }

}

// 输出:

// (1秒后)收集到数据:奇数: 1

// (2秒后)收集到数据:奇数: 3

三、Flow 的创建方式(Flow Builders)

Kotlin 提供了多种构建 Flow 的方式,覆盖不同场景:

1. flow 构建器(最常用)

通过 lambda 表达式自定义数据发射逻辑,支持挂起函数(如delay、网络请求),是最灵活的创建方式。

// 模拟网络请求获取用户列表

fun getUserFlow(): Flow<User> = flow {

   // 模拟网络延迟

   delay(1500)

   // 发射多个用户数据

   emit(User(id = 1, name = "Alice"))

   delay(1000)

   emit(User(id = 2, name = "Bob"))

}

data class User(val id: Int, val name: String)

2. flowOf 构建器

用于发射固定数量的已知数据,适用于静态数据场景。

// 发射3个字符串数据

val stringFlow = flowOf("Apple", "Banana", "Cherry")

// 终端操作:收集数据

runBlocking {

   stringFlow.collect { println(it) }&#x20;

   // 输出:Apple、Banana、Cherry(无延迟,同步发射)

}

3. asFlow 扩展函数

集合(List/Set)、序列(Sequence)、范围(Range) 转换为 Flow,适用于已有数据集合的场景。

// 1. 范围转Flow

val rangeFlow = (1..5).asFlow()

// 2. 列表转Flow

val listFlow = listOf("Kotlin", "Java", "Swift").asFlow()

// 3. 序列转Flow

val sequenceFlow = sequence { yield(10); yield(20) }.asFlow()

runBlocking {

   rangeFlow.collect { println(it) } // 输出:1、2、3、4、5

}

4. 其他构建器

  • emptyFlow():创建空流(不发射任何数据),适用于默认返回值场景。

  • channelFlow:基于 Channel 构建 Flow,支持多协程并发发射数据(后续 “热流” 部分详解)。

  • callbackFlow:将回调接口(如 Java 回调)转换为 Flow,解决回调地狱问题(如 Android 的 LocationCallback)。

// callbackFlow示例:将回调转换为Flow

fun getLocationFlow(): Flow<Location> = callbackFlow {

   val locationCallback = object : LocationCallback() {

       override fun onLocationResult(result: LocationResult) {

           // 向Flow发射位置数据

          trySend(result.lastLocation) // 非挂起函数,线程安全

       }

   }

   // 注册回调

   locationManager.requestLocationUpdates(locationCallback)

   // 关闭Flow时取消回调(避免内存泄漏)

   awaitClose {

       locationManager.removeUpdates(locationCallback)

   }

}

四、Flow 的操作符

Flow 提供了丰富的操作符,用于数据处理、生命周期控制、背压管理等,分为以下几类:

1. 数据转换操作符

对发射的数据进行格式转换或结构调整。

操作符功能描述示例代码
map将数据转换为另一种类型flowOf(1,2,3).map { it * 2 } → 发射 2、4、6
flatMapConcat转换为新 Flow 并顺序执行(前一个 Flow 完成再执行下一个)flowOf(1,2).flatMapConcat { flow { emit(it); delay(1000) } } → 1(延迟 1 秒)、2(延迟 1 秒)
flatMapMerge转换为新 Flow 并并发执行(同时执行多个 Flow)flowOf(1,2).flatMapMerge { flow { emit(it); delay(1000) } } → 1(1 秒后)、2(1 秒后,共 1 秒)
flatMapLatest转换为新 Flow,若前一个 Flow 未完成则取消,只保留最新 FlowflowOf(1,2).flatMapLatest { flow { delay(500); emit(it) } } → 仅发射 2

2. 过滤操作符

筛选符合条件的数据,剔除无效数据。

操作符功能描述示例代码
filter保留满足条件的数据flowOf(1,2,3,4).filter { it % 2 == 0 } → 发射 2、4
distinctUntilChanged仅保留与前一个数据不同的值(去重连续重复数据)flowOf(1,1,2,2,3).distinctUntilChanged() → 发射 1、2、3
take仅保留前 N 个数据,之后关闭流flowOf(1,2,3,4).take(2) → 发射 1、2
drop跳过前 N 个数据,保留后续数据flowOf(1,2,3,4).drop(2) → 发射 3、4

3. 背压处理操作符

背压(Backpressure)指数据发射速度 > 数据收集速度时的不平衡问题,Flow 通过以下操作符解决:

操作符功能描述适用场景
buffer为流设置缓冲区,暂存发射的数据(默认容量 64)发射速度略快于收集速度
conflate忽略中间数据,仅保留最新数据(“合并” 数据)发射速度远快于收集速度(如实时日志)
collectLatest取消前一次未完成的收集,仅处理最新数据仅需最新结果(如搜索输入联想)
// 背压示例:conflate(合并数据)

runBlocking {

   flow {

       for (i in 1..5) {

           delay(100) // 发射速度快(100ms/次)

           emit(i)

       }

   }

   .conflate() // 合并中间数据

   .collect {

       delay(500) // 收集速度慢(500ms/次)

       println("收集到:$it")

   }

   // 输出:1、3、5(跳过2、4,因收集时已产生新数据)

}

4. 终端操作符

触发 Flow 的数据发射,是 Flow 执行的 “开关”,必须调用终端操作 Flow 才会生效。

操作符功能描述示例代码
collect最常用,逐个收集数据(挂起函数)flowOf(1,2).collect { println(it) }
toList/toSet将数据收集为 List/Set(挂起函数)val list = flowOf(1,2).toList() → [1,2]
first/last仅获取第一个 / 最后一个数据,之后关闭流val first = flowOf(1,2,3).first() → 1
count统计数据总数val count = flowOf(1,2,3).count() → 3
onCompletion流完成(正常 / 异常)时执行回调flowOf(1).onCompletion { println("流结束") }.collect { } → 输出 “流结束”

五、热流:StateFlow 与 SharedFlow

默认的 Flow 是冷流(一对一订阅,无订阅者不发射),而 StateFlow 和 SharedFlow 是热流(一对多订阅,有订阅者即可能发射数据,数据可被多个订阅者共享),主要用于状态管理和事件分发。

1. StateFlow(状态流)

核心特性
  • 状态持有:持有一个 “当前状态值”,新订阅者会立即收到当前最新值(粘性)。

  • 值更新触发:仅当新值与旧值不同时,才会通知订阅者(避免无效更新)。

  • 生命周期感知:建议结合repeatOnLifecycle使用(Android),避免内存泄漏。

使用步骤
  1. 定义MutableStateFlow(可修改状态,内部使用)和StateFlow(只读状态,对外暴露)。

  2. 通过value属性更新状态。

  3. 订阅StateFlow获取状态变化。

import kotlinx.coroutines.delay

import kotlinx.coroutines.flow.MutableStateFlow

import kotlinx.coroutines.flow.StateFlow

import kotlinx.coroutines.flow.asStateFlow

import kotlinx.coroutines.launch

import kotlinx.coroutines.runBlocking

class CounterViewModel {

   // 1. 内部可修改的MutableStateFlow(初始值0)

   private val _countState = MutableStateFlow(0)

   // 2. 对外暴露的只读StateFlow

   val countState: StateFlow<Int> = _countState.asStateFlow()

   // 更新状态的方法

   fun increment() {

       _countState.value += 1 // 仅当新值≠旧值时触发通知

   }

   // 模拟异步更新状态

   suspend fun asyncIncrement() {

       delay(1000)

       _countState.value += 1

   }

}

// 测试StateFlow

fun main() = runBlocking {

   val viewModel = CounterViewModel()

   // 订阅者1

   launch {

       viewModel.countState.collect { count ->

           println("订阅者1:当前计数=$count")

       }

   }

   // 订阅者2(延迟1秒订阅,仍能收到最新值)

   delay(1500)

   launch {

       viewModel.countState.collect { count ->

           println("订阅者2:当前计数=$count")

       }

   }

   // 更新状态

   viewModel.increment()

   delay(500)

   viewModel.asyncIncrement()

}

// 输出:

// 订阅者1:当前计数=0(初始值)

// 订阅者1:当前计数=1(increment触发)

// 订阅者2:当前计数=1(延迟订阅,收到最新值)

// 订阅者1:当前计数=2(asyncIncrement触发)

// 订阅者2:当前计数=2(收到更新)

2. SharedFlow(共享流)

核心特性
  • 事件分发:用于发送 “一次性事件”(如弹窗提示、导航指令),默认不持有状态(非粘性,新订阅者不会收到历史事件)。

  • 可配置参数:支持配置replay(重放历史事件数)、extraBufferCapacity(额外缓冲区容量)、onBufferOverflow(缓冲区满时的策略)。

  • 手动触发:通过emittryEmit发射事件,无默认值。

使用步骤
  1. 定义MutableSharedFlow(内部可发射)和SharedFlow(对外只读)。

  2. 配置参数(如replay = 0表示不重放历史事件)。

  3. 发射事件与订阅事件。

import kotlinx.coroutines.delay

import kotlinx.coroutines.flow.MutableSharedFlow

import kotlinx.coroutines.flow.SharedFlow

import kotlinx.coroutines.flow.asSharedFlow

import kotlinx.coroutines.launch

import kotlinx.coroutines.runBlocking

class EventViewModel {

   // 1. 内部可发射的MutableSharedFlow(replay=0:不重放历史事件)

   private val _eventFlow = MutableSharedFlow<String>(

       replay = 0,

       extraBufferCapacity = 1, // 额外缓冲区容量(避免发射时无订阅者导致事件丢失)

       onBufferOverflow = MutableSharedFlow.OnBufferOverflow.DROP_OLDEST // 缓冲区满时丢弃旧事件

   )

   // 2. 对外暴露的只读SharedFlow

   val eventFlow: SharedFlow<String> = _eventFlow.asSharedFlow()

   // 发射事件

   suspend fun sendEvent(message: String) {

       _eventFlow.emit(message) // 挂起函数,缓冲区满时挂起

       // _eventFlow.tryEmit(message) // 非挂起函数,缓冲区满时返回false

   }

}

// 测试SharedFlow

fun main() = runBlocking {

   val viewModel = EventViewModel()

   // 订阅者1(立即订阅)

   launch {

       viewModel.eventFlow.collect { event ->

           println("订阅者1收到事件:$event")

       }

   }

   // 发射事件(订阅者1能收到)

   viewModel.sendEvent("弹窗提示:登录成功")

   delay(1000)

   // 订阅者2(延迟订阅,不会收到之前的事件)

   launch {

       viewModel.eventFlow.collect { event ->

           println("订阅者2收到事件:$event")

       }

   }

   // 发射事件(订阅者1和2都能收到)
   viewModel.sendEvent("导航至首页")

}

// 输出:

// 订阅者1收到事件:弹窗提示:登录成功

// 订阅者1收到事件:导航至首页

// 订阅者2收到事件:导航至首页

3. StateFlow 与 SharedFlow 的区别

特性StateFlowSharedFlow
状态持有持有当前最新值(粘性)不持有状态(默认非粘性)
新订阅者通知立即收到最新值仅收到订阅后的新事件(默认)
数据更新触发新值≠旧值时触发调用emit/tryEmit时触发
适用场景状态管理(如计数器、用户信息)事件分发(如弹窗、导航、通知)

六、Flow 的异常处理

Flow 的异常处理需通过catch操作符(捕获上游异常)和onCompletion(处理流结束时的异常),避免崩溃扩散。

1. catch 操作符

捕获上游流(当前catch之前的操作) 的异常,并可发射备用数据或重新抛出异常。

runBlocking {

   flow {

       emit(1)

       throw IllegalArgumentException("数据异常") // 抛出异常

       emit(2) // 不会执行

   }
   .catch { e ->

       println("捕获异常:${e.message}")

       emit(-1) // 发射备用数据

   }

   .collect { println("收集到:$it") }

}

// 输出:

// 收集到:1

// 捕获异常:数据异常

// 收集到:-1

2. onCompletion 操作符

在流正常结束异常结束时执行回调,可通过cause参数判断是否发生异常。

runBlocking {

   flow {

       emit(1)

       throw RuntimeException("流执行失败")

   }

   .onCompletion { cause ->

       if (cause != null) {

           println("流异常结束:\${cause.message}")

       } else {

           println("流正常结束")

       }

   }

   .catch { e -> println("捕获异常:\${e.message}") }

   .collect { println(it) }

}

// 输出:

// 收集到:1

// 捕获异常:流执行失败

// 流异常结束:流执行失败

七、Flow 的上下文切换

Flow 的上下文(CoroutineContext)分为发射上下文(数据发射所在的线程)和收集上下文(数据收集所在的线程),通过flowOn操作符控制发射上下文。

关键规则

  1. flowOn 仅影响上游流flowOn指定的上下文仅对其之前的流操作生效(即数据发射阶段)。

  2. 收集上下文由协程决定collect所在的协程上下文(如Dispatchers.MainDispatchers.IO)即为收集上下文,flowOn无法修改。

import kotlinx.coroutines.Dispatchers

import kotlinx.coroutines.flow.flow

import kotlinx.coroutines.flow.flowOn

import kotlinx.coroutines.runBlocking

fun main() = runBlocking(Dispatchers.Main) { // 收集上下文:Main线程

   flow {

       println("发射数据线程:${Thread.currentThread().name}") // IO线程

       emit(1)

   }

   .flowOn(Dispatchers.IO) // 发射上下文:IO线程(仅影响上游)

   .collect {

       println("收集数据线程:\${Thread.currentThread().name}") // Main线程

   }

}

// 输出:

// 发射数据线程:DefaultDispatcher-worker-1(IO线程)

// 收集数据线程:main(Main线程)

八、Flow 在 Android 中的实践

在 Android 开发中,Flow 常与 Jetpack 组件(ViewModel、Lifecycle、Room)结合,用于数据层到 UI 层的通信。

示例:Room 数据库 + ViewModel+Flow

  1. Room Dao 层:返回 Flow(Room 自动支持 Flow,数据变化时自动发射新数据)
@Dao

interface UserDao {

   @Query("SELECT * FROM user")

   fun getAllUsers(): Flow<List<User>> // Room返回Flow,数据更新时自动通知

   @Insert

   suspend fun insertUser(user: User)

}
  1. ViewModel 层:持有 Flow,对外暴露只读流
class UserViewModel(

   private val userDao: UserDao

) : ViewModel() {

   // 对外暴露只读Flow(结合Dispatchers.IO发射数据)

   val allUsers: Flow<List<User>> = userDao.getAllUsers()

       .flowOn(Dispatchers.IO)

       .catch { e ->

           // 异常处理

           _errorEvent.emit("获取用户失败:${e.message}")

       }

   // 插入用户(挂起函数,在ViewModelScope中执行)

   fun insertUser(user: User) {

       viewModelScope.launch {

           userDao.insertUser(user)

       }

   }

   // 错误事件(SharedFlow)

   private val _errorEvent = MutableSharedFlow<String>()

   val errorEvent: SharedFlow<String> = _errorEvent.asSharedFlow()

}
  1. UI 层(Activity/Fragment):订阅 Flow,结合生命周期避免内存泄漏
class UserActivity : AppCompatActivity() {

   private val viewModel: UserViewModel by viewModels()

   override fun onCreate(savedInstanceState: Bundle?) {

       super.onCreate(savedInstanceState)

       // 结合repeatOnLifecycle(Lifecycle 2.4.0+),在STARTED状态订阅,STOPPED状态取消

       lifecycleScope.launch {

           repeatOnLifecycle(Lifecycle.State.STARTED) {

               // 订阅用户数据,更新UI

               viewModel.allUsers.collect { users ->

                  binding.recyclerView.adapter = UserAdapter(users)

               }

           }

       }

       // 订阅错误事件

       lifecycleScope.launch {

           repeatOnLifecycle(Lifecycle.State.STARTED) {

               viewModel.errorEvent.collect { errorMsg ->

                   Toast.makeText(this@UserActivity, errorMsg, Toast.LENGTH_SHORT).show()

               }

           }

       }

   }

}

九、Flow 的注意事项

  1. 冷流特性:无终端操作(如collect)时,Flow 不会执行任何逻辑,避免无效计算。

  2. 上下文切换flowOn仅修改发射上下文,收集上下文由collect所在协程决定,避免多次切换上下文导致性能问题。

  3. 内存泄漏:Android 中需结合lifecycleScoperepeatOnLifecycle,避免 Flow 订阅者持有 Activity/Fragment 引用导致内存泄漏。

  4. StateFlow 粘性问题:新订阅者会收到最新值,若需 “一次性事件”(非粘性),应使用 SharedFlow(replay=0)。

  5. 背压处理:发射速度远快于收集速度时,需使用conflatecollectLatest,避免缓冲区溢出。