本文译自「Cancellable Flows in Kotlin Coroutines: The Complete Guide to Flow Cancellation Techniques」,原文链接proandroiddev.com/cancellable...,由Sahil Thakar发布于2025年7月21日。
译者按: 本文并不是Flow的基础教程,而是专门讲解如何取消flow的,适合对Flow有一定基础的同学。如果对Flow还不够熟悉,可以先行阅读一下之前的文章:

大家好!👋
今天我们将深入探讨 Kotlin Flows 中最重要的一个方面------Flow取消。如果你一直在使用 Flows,你可能遇到过需要停止长时间运行的 Flow 操作的情况,这可能是因为用户离开了某个页面,或者网络请求耗时过长,又或者你只是想避免不必要的资源消耗。
Flow 取消不仅仅是调用 cancel()
并祈祷好运。它有多种复杂的技术,每种技术都有各自的用例和细微差别。那么,让我们揭开帷幕,探索可取消 Flows 的世界吧! 🚀
为何取消Flow如此重要
在深入探讨相关技术之前,我们先来了解一下取消机制为何如此重要:
- 资源管理:防止内存泄漏和不必要的 CPU 占用
- 用户体验:当用户离开时停止过时的操作
- 网络效率:取消不再需要的待处理请求
- 电池续航:减少移动设备上的后台处理
方法 1:Job取消 --- 基础
取消Flow最基本的方式是通过作业取消(Job Cancellation)。每个协程都有一个作业(Job),当你取消该作业时,该协程范围内运行的所有Flow也会被取消。
让我们来看看实际操作:
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.seconds
suspend fun main() {
val job = CoroutineScope(Dispatchers.Default).launch {
createNumberFlow()
.collect { value ->
println("Received: $value")
}
}
delay(3.seconds)
println("Cancelling job...")
job.cancel()
delay(1.seconds)
println("Program finished")
}
fun createNumberFlow() = flow {
repeat(10) { i ->
println("Emitting: $i")
emit(i)
delay(1.seconds)
}
}
Output:
apache
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Cancelling job...
Program finished
这个过程里面到底发生了什么?
当你调用 job.cancel()
时,它会向协程发送一个取消信号。Flow构建器 (flow { }
) 是取消协作式 的,这意味着它会在 delay()
和 emit()
等挂起点检查取消情况。一旦取消,Flow就会停止发出新值,收集器也会停止接收它们。
但这里有一个有趣的问题------如果你的Flow没有挂起点怎么办?
kotlin
fun nonCancellableFlow() = flow {
repeat(1000000) { i ->
emit(i) // 无挂起点!
// 即使取消后仍将继续运行
}
}
此Flow不会考虑取消操作,因为没有挂起点。要解决这个问题,你可以使用"ensureActive()":
kotlin
fun cancellableFlow() = flow {
repeat(1000000) { i ->
ensureActive() // 会有对取消的检测
emit(i)
}
}
使用结构化并发实现高级作业取消
让我们探索一个使用结构化并发(Structured Concurrency)的更复杂的场景:
kotlin
class DataRepository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun fetchDataStream(): Flow<String> = flow {
repeat(Int.MAX_VALUE) { i ->
emit("Data item $i")
delay(500.milliseconds)
}
}.flowOn(Dispatchers.IO)
fun startFetching(): Job {
return scope.launch {
fetchDataStream()
.catch { e -> println("Error: ${e.message}") }
.collect { data ->
println("Processing: $data")
}
}
}
fun cleanup() {
scope.cancel("Repository is being cleaned up")
}
}
// Usage
suspend fun main() {
val repository = DataRepository()
val fetchJob = repository.startFetching()
delay(3.seconds)
println("Cleaning up repository...")
repository.cleanup()
delay(1.seconds)
println("Done")
}
Output:
lasso
Processing: Data item 0
Processing: Data item 1
Processing: Data item 2
Processing: Data item 3
Processing: Data item 4
Processing: Data item 5
Cleaning up repository...
Done
方法 2:withTimeout --- 基于时间的取消
有时,如果Flow操作耗时过长,你需要取消它。"withTimeout"非常适合这种情况。它会创建一个定时炸弹 ⏰ --- 如果操作未在指定时间内完成,则会抛出"TimeoutCancellationException"异常。
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.time.Duration.Companion.seconds
suspend fun main() {
try {
withTimeout(5.seconds) {
slowDataFlow()
.collect { value ->
println("Received: $value")
}
}
} catch (e: TimeoutCancellationException) {
println("Operation timed out: ${e.message}")
}
}
fun slowDataFlow() = flow {
repeat(10) { i ->
println("Emitting: $i")
emit(i)
delay(1.seconds) // Each emission takes 1 second
}
}
Output:
avrasm
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Operation timed out: Timed out waiting for 5000 ms
withTimeoutOrNull --- 优雅的超时处理
如果你不想出现异常,请使用 withTimeoutOrNull
:
kotlin
suspend fun main() {
val result = withTimeoutOrNull(3.seconds) {
fastDataFlow()
.toList() // Collect all values into a list
}
when (result) {
null -> println("Operation timed out")
else -> println("Completed with results: $result")
}
}
fun fastDataFlow() = flow {
repeat(5) { i ->
emit(i)
delay(500.milliseconds)
}
}
Output:
nix
Completed with results: [0, 1, 2, 3, 4]
真实示例:带超时的网络请求
以下是使用超时进行网络请求的实际示例:
kotlin
class ApiService {
suspend fun fetchUserData(userId: String): Flow<UserData> = flow {
// Simulate network delay
delay(2.seconds)
emit(UserData(userId, "John Doe", "john@example.com"))
delay(1.seconds)
emit(UserData(userId, "John Doe", "john.doe@example.com")) // Updated email
}
}
data class UserData(val id: String, val name: String, val email: String)
suspend fun main() {
val apiService = ApiService()
try {
withTimeout(4.seconds) {
apiService.fetchUserData("123")
.collect { userData ->
println("User data updated: $userData")
}
}
} catch (e: TimeoutCancellationException) {
println("Network request timed out. Please check your connection.")
}
}
Output:
less
User data updated: UserData(id=123, name=John Doe, email=john@example.com)
User data updated: UserData(id=123, name=John Doe, email=john.doe@example.com)
方法 3:takeWhile --- 条件取消
takeWhile
是一个强大的运算符,它根据条件 取消Flow。只要谓词返回 true
,它就会继续发出值;一旦返回 false
,它就会停止(取消)。
kotlin
suspend fun main() {
numberSequence()
.takeWhile { it < 5 } // Stop when value >= 5
.collect { value ->
println("Received: $value")
}
println("Flow completed")
}
fun numberSequence() = flow {
repeat(10) { i ->
println("Emitting: $i")
emit(i)
delay(500.milliseconds)
}
}
Output:
apache
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5
Flow completed
注意,该Flow发送了 5 个值,但却没有收到,因为 takeWhile
在条件变为 false 时停止了收集。
takeWhile 与 filter --- 理解区别
许多开发人员会混淆 takeWhile
和 filter
。让我们看看它们的区别:
kotlin
suspend fun main() {
println("=== Using filter ===")
numberSequence()
.filter { it < 5 } // 过滤值但不停止flow
.collect { value ->
println("Received: $value")
}
println("\n=== Using takeWhile ===")
numberSequence()
.takeWhile { it < 5 } // 完全停止flow
.collect { value ->
println("Received: $value")
}
}
Output:
vbnet
=== Using filter ===
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5
Emitting: 6
Emitting: 7
Emitting: 8
Emitting: 9
=== Using takeWhile ===
Emitting: 0
Received: 0
Emitting: 1
Received: 1
Emitting: 2
Received: 2
Emitting: 3
Received: 3
Emitting: 4
Received: 4
Emitting: 5
关键区别 :filter
会继续执行Flow,但会跳过不需要的值,而 takeWhile
则会完全终止Flow。
实际 takeWhile 示例
示例 1:电池电量监控
kotlin
fun batteryLevelFlow() = flow {
var batteryLevel = 100
while (true) {
emit(batteryLevel)
batteryLevel = (batteryLevel - (1..5).random()).coerceAtLeast(0)
delay(1.seconds)
}
}
suspend fun main() {
batteryLevelFlow()
.takeWhile { it > 20 } // Stop monitoring when battery is low
.collect { level ->
println("Battery level: $level%")
if (level <= 30) {
println("⚠️ Low battery warning!")
}
}
println("🔋 Battery critically low! Stopping monitoring.")
}
示例 2:股票价格监控
kotlin
data class StockPrice(val symbol: String, val price: Double)
fun stockPriceFlow(symbol: String) = flow {
var price = 100.0
while (true) {
price += (-5.0..5.0).random()
emit(StockPrice(symbol, price))
delay(1.seconds)
}
}
suspend fun main() {
val stopLossPrice = 90.0
stockPriceFlow("AAPL")
.takeWhile { it.price > stopLossPrice } // Stop loss triggered
.collect { stock ->
println("${stock.symbol}: $${String.format("%.2f", stock.price)}")
}
println("🛑 Stop loss triggered! Stopping price monitoring.")
}
方法 4:take --- 基于数量的取消
take
在发出特定数量的项目后取消Flow:
kotlin
suspend fun main() {
infiniteFlow()
.take(5) // 仅取前5个元素
.collect { value ->
println("Received: $value")
}
println("Collected exactly 5 items")
}
fun infiniteFlow() = flow {
var counter = 0
while (true) {
emit(counter++)
delay(300.milliseconds)
}
}
方法 5:cancellable() --- 使Flow具备取消感知能力
有时,你的Flow本身并不支持取消操作。cancellable()
运算符可以使它们对取消操作做出响应:
kotlin
suspend fun main() {
val job = CoroutineScope(Dispatchers.Default).launch {
heavyComputationFlow()
.cancellable() // 使flow感知取消
.collect { value ->
println("Processed: $value")
}
} delay(2.seconds)
println("Cancelling...")
job.cancel()
delay(500.milliseconds)
println("Done")
}
fun heavyComputationFlow() = flow {
repeat(1000) { i ->
// 模拟无挂起点的繁重计算
val result = (1..1000).map { it * it }.sum()
emit("Result $i: $result")
}
}
方法 6:first() 和条件终止运算符
你关于 first { condition }
的说法完全正确!这是一种强大的取消技术,Flow会一直收集,直到找到第一个符合条件的元素,然后取消Flow并返回该元素。
kotlin
suspend fun main() {
val result = numberFlow()
.first { it > 5 } // Cancels as soon as it finds first value > 5
println("First value > 5: $result")
}
fun numberFlow() = flow {
repeat(20) { i ->
println("Emitting: $i")
emit(i)
delay(200.milliseconds)
}
}
Output:
apache
Emitting: 0
Emitting: 1
Emitting: 2
Emitting: 3
Emitting: 4
Emitting: 5
Emitting: 6
First value > 5: 6
注意,在找到第一个大于 5 的值后,Flow是如何停止发射 的。这与 filter
不同,后者会继续整个Flow。
对比 first() 和 firstOrNull()
kotlin
suspend fun main() {
// first() - 如果未找到则抛出异常
try {
val result1 = shortFlow().first { it > 10 }
println("Found: $result1")
} catch (e: NoSuchElementException) {
println("No element found matching condition")
}
// firstOrNull() - 如果未找到则返回 null
val result2 = shortFlow().firstOrNull { it > 10 }
println("Result: $result2")
}
fun shortFlow() = flowOf(1, 2, 3, 4, 5)
Output:
sql
No element found matching condition
Result: null
真实示例:查找可用服务器
kotlin
data class Server(val name: String, val responseTime: Int)
fun checkServers() = flow {
val servers = listOf("server1", "server2", "server3", "server4")
servers.forEach { serverName ->
println("Checking $serverName...")
delay(500.milliseconds) // 模拟网络检查
val responseTime = (100..800).random()
emit(Server(serverName, responseTime))
}
}
suspend fun main() {
val fastServer = checkServers()
.first { it.responseTime < 300 } // 找到第一个快速服务器并停止
println("Using fast server: ${fastServer.name} (${fastServer.responseTime}ms)")
}
方法 7:single() --- 期望恰好一个元素
single()
与 first()
类似,但它期望恰好一个元素。找到第一个元素后会取消,但如果有多个元素,则会引发异常。
kotlin
suspend fun main() {
// 这能行 - 恰好有一个元素匹配
val result1 = flowOf(1, 2, 3, 4, 5)
.single { it == 3 }
println("Single result: $result1")
// 这会抛异常 - 有多个元素匹配
try {
val result2 = flowOf(1, 2, 3, 4, 5)
.single { it > 2 } // 3, 4, 5 all match!
println("Result: $result2")
} catch (e: IllegalArgumentException) {
println("Error: Multiple elements found - ${e.message}")
}
}
Output:
subunit
Single result: 3
Error: Multiple elements found - Flow has more than one element matching the predicate.
方法 8:any()、all()、none() --- 布尔条件取消
这些运算符可根据布尔条件提供提前取消功能:
any() --- 首次匹配时取消
kotlin
suspend fun main() {
val hasLargeNumber = numberFlow()
.any { it > 15 } // 一旦发现第一个值 > 15 则取消
println("Has number > 15: $hasLargeNumber")
}
fun numberFlow() = flow {
repeat(30) { i ->
println("Checking: $i")
emit(i)
delay(100.milliseconds)
}
}
Output:
yaml
Checking: 0
Checking: 1
...
Checking: 15
Checking: 16
Has number > 15: true
all() --- 第一次不匹配时取消
kotlin
suspend fun main() {
val allSmall = flowOf(1, 2, 3, 4, 5, 15, 6, 7)
.all { it < 10 } // 一旦发现第一个值 >= 10 就取消
println("All numbers < 10: $allSmall")
}
none() --- 第一次匹配时取消
kotlin
suspend fun main() {
val noLargeNumbers = flowOf(1, 2, 3, 4, 5, 15, 6, 7)
.none { it > 10 } // 一旦发现第一个值 > 10 则取消
println("No numbers > 10: $noLargeNumbers")
}
方法 9:transformWhile --- 高级条件转换
transformWhile
是 takeWhile
的更强大版本,它允许你转换元素,并且具有更灵活的发射行为:
kotlin
suspend fun main() {
numberFlow()
.transformWhile { value ->
if (value < 5) {
emit("Value: $value")
emit("Double: ${value * 2}") // Can emit multiple times
true // Continue
} else {
emit("Final: $value") // Can emit the "stopping" element
false // Stop here
}
}
.collect { println(it) }
}
Output:
apache
Emitting: 0
Value: 0
Double: 0
Emitting: 1
Value: 1
Double: 2
Emitting: 2
Value: 2
Double: 4
Emitting: 3
Value: 3
Double: 6
Emitting: 4
Value: 4
Double: 8
Emitting: 5
Final: 5
对比transformWhile和takeWhile
kotlin
suspend fun main() {
println("=== takeWhile ===")
flowOf(1, 2, 3, 4, 5, 6)
.takeWhile { it < 4 }
.collect { println("Received: $it") }
println("\n=== transformWhile ===")
flowOf(1, 2, 3, 4, 5, 6)
.transformWhile { value ->
if (value < 4) {
emit("Valid: $value")
true
} else {
emit("Stopping at: $value") // 仍可以发出正被停止的元素!
false
}
}
.collect { println(it) }
}
Output:
asciidoc
=== takeWhile ===
Received: 1
Received: 2
Received: 3
=== transformWhile ===
Valid: 1
Valid: 2
Valid: 3
Stopping at: 4
方法 10:collectLatest --- 取消上一次终端处理
每当发出新值时,collectLatest
都会取消上一次收集操作,即上一次的终端处理:
kotlin
suspend fun main() {
fastEmittingFlow()
.collectLatest { value ->
println("Processing $value...")
delay(1.seconds) // Slow processing
println("Finished processing $value")
}
}
fun fastEmittingFlow() = flow {
repeat(5) { i ->
emit(i)
delay(300.milliseconds) // Fast emission
}
}
Output:
gams
Processing 0...
Processing 1...
Processing 2...
Processing 3...
Processing 4...
Finished processing 4
只有最后一个值会被完全处理,因为每次新的发送都会取消之前的处理。
collectLatest 实用示例:即时搜索的实现
kotlin
class SearchManager {
private val _searchQuery = MutableStateFlow("")
suspend fun startSearching() {
_searchQuery
.filter { it.isNotBlank() }
.collectLatest { query ->
println("Searching for: $query")
delay(2.seconds) // Simulate API call
println("Results for: $query")
}
}
fun updateQuery(query: String) {
_searchQuery.value = query
}
}
suspend fun main() {
val searchManager = SearchManager()
val job = CoroutineScope(Dispatchers.Default).launch {
searchManager.startSearching()
}
// Simulate user typing
searchManager.updateQuery("k")
delay(500.milliseconds)
searchManager.updateQuery("ko")
delay(500.milliseconds)
searchManager.updateQuery("kot")
delay(500.milliseconds)
searchManager.updateQuery("kotlin")
delay(3.seconds)
job.cancel()
}
Output:
nestedtext
Searching for: k
Searching for: ko
Searching for: kot
Searching for: kotlin
Results for: kotlin
只有"kotlin"会获得完整的搜索结果,因为之前的搜索已被新查询取消。
方法 11:使用 SharedFlow 和 StateFlow 自定义取消
对于更复杂的场景,你可能需要自定义取消逻辑:
kotlin
class DataManager {
private val _dataFlow = MutableSharedFlow<String>()
val dataFlow = _dataFlow.asSharedFlow()
private var isActive = true
suspend fun startEmitting() {
while (isActive) {
_dataFlow.emit("Data at ${System.currentTimeMillis()}")
delay(1.seconds)
}
}
fun stop() {
isActive = false
}
}
suspend fun main() {
val dataManager = DataManager()
val job = CoroutineScope(Dispatchers.Default).launch {
dataManager.startEmitting()
}
val collectorJob = CoroutineScope(Dispatchers.Default).launch {
dataManager.dataFlow.collect { data ->
println("Received: $data")
}
}
delay(3.seconds)
println("Stopping data manager...")
dataManager.stop()
delay(1.seconds)
job.cancel()
collectorJob.cancel()
println("All jobs cancelled")
}
最佳实践及各技术的使用时机
1. Job取消
- 使用时机:你需要取消整个协程范围
- 最佳用途:Activity/Fragment 生命周期管理、代码库清理
- 记住:始终取消父作业以避免内存泄漏
2. withTimeout/withTimeoutOrNull
- 使用时机:操作有时间限制
- 最佳用途:网络请求、文件操作、用户输入等待
- 记住 :考虑使用
withTimeoutOrNull
进行优雅处理
3. takeWhile
- 使用时机:你有基于条件的停止条件
- 最佳用途:监控系统、用户输入验证、基于阈值的操作
- 记住:一旦条件变为 false,Flow就会停止
4. take
- 使用时机:你需要特定数量的项目
- 最佳用途:分页、采样、测试场景
- 记住:简单且可预测的行为
5. cancellable()
- 使用场景:处理 CPU 密集型且无挂起点的Flow
- 最适合:数学计算、数据处理
- 记住:会增加开销,因此仅在必要时使用
性能相关注意事项
不同的取消技术对性能的影响不尽相同:
kotlin
suspend fun performanceComparison() {
println("=== Performance Test ===") // 测试 1:作业取消(最快)
val time1 = measureTimeMillis {
val job = CoroutineScope(Dispatchers.Default).launch {
repeat(1000000) {
// Heavy work
}
}
job.cancel()
}
println("Job cancellation: ${time1}ms") // 测试 2:takeWhile(条件开销)
val time2 = measureTimeMillis {
flow {
repeat(1000) { emit(it) }
}.takeWhile { it < 500 }
.collect { }
}
println("takeWhile: ${time2}ms") // 测试 3:withTimeout(异常开销)
val time3 = measureTimeMillis {
try {
withTimeout(1.milliseconds) {
repeat(1000000) {
// Some work
}
}
} catch (e: TimeoutCancellationException) {
// Expected
}
}
println("withTimeout: ${time3}ms")
}
常见陷阱及避免方法
陷阱 1:忘记检查 CPU 密集型操作中的取消操作
kotlin
// ❌ 错误------没考虑取消
fun badFlow() = flow {
repeat(1000000) { i ->
heavyComputation()
emit(i)
}
}
// ✅ 正确 - 检查取消
fun goodFlow() = flow {
repeat(1000000) { i ->
ensureActive() // or yield()
heavyComputation()
emit(i)
}
}
陷阱 2:不处理 TimeoutCancellationException
kotlin
// ❌ 错误 - 异常会导致应用程序崩溃
suspend fun badTimeout() {
withTimeout(1.seconds) {
longRunningOperation()
}
}
// ✅ 正确 - 正确的异常处理
suspend fun goodTimeout() {
try {
withTimeout(1.seconds) {
longRunningOperation()
}
} catch (e: TimeoutCancellationException) {
println("Operation timed out, handling gracefully")
}
}
陷阱 3:混淆 takeWhile 和 filter
kotlin
// 记住:takeWhile 会停止Flow,filter 只会跳过值
flow { emit(1); emit(2); emit(3) }
.takeWhile { it < 2 } // 发射:1(然后停止)
.filter { it < 2 } // 这甚至不会运行,因为 takeWhile 停止了flow
结论
Flow取消是一项强大的功能,如果使用得当,可以显著提升应用的性能和用户体验。以下是简要回顾:
- 作业取消(Job cancellation) 用于生命周期管理
- withTimeout 用于时间限制操作
- takeWhile 用于基于条件的停止
- take 用于基于计数的限制
- cancellable() 用于使非合作性Flow具有响应性
请记住,有效地取消Flow的关键在于理解你的用例并选择正确的技术。不要想太多------从最简单的解决问题的方法入手,然后再进行优化。
祝你使用愉快!🌊
你最喜欢的Flow取消技术是什么?你遇到过什么有趣的边缘情况吗?请在下方留言,让我们一起讨论!💬
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!