Flow 责任链模式图解

Flow 责任链模式图解

背景:FCM 推送消息过滤问题

实际场景

在我们的应用中遇到一个典型问题:

问题描述:

  • FCM(Firebase Cloud Messaging)推送消息
  • 一分钟内可能发送多个相同的消息
  • 每次收到消息就会触发网络请求
  • 导致重复请求,浪费带宽和服务器资源

解决方案:

  • 需要过滤重复消息,在一分钟内多个消息只有第一个响应
  • 实现两个 Flow 操作符来解决不同场景的问题
kotlin 复制代码
/**
 * 冷流节流操作符:在时间窗口内只执行第一次,避免频繁执行
 * @param durationMillis 节流时间窗口(毫秒),在此时间内只会执行一次
 * @return Flow<T> 应用节流后的Flow
 */
fun <T> Flow<T>.throttleFirst(
    durationMillis: Long
): Flow<T> {
    var lastCurrentTime = 0L
    return flow {
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastCurrentTime > durationMillis) {
            lastCurrentTime = currentTime
            // 收集并缓存所有发射的值
            this@throttleFirst.collect { value ->
                emit(value)
            }
        }
    }
}
```kotlin
 /* 
 热流
   @param durationMillis 节流时间窗口(毫秒)
 * @param predicate 判断条件,返回 true 表示该值需要节流处理,false 表示直接通过
 * @return Flow<T> 应用条件节流后的 Flow
 */
fun <T> Flow<T>.throttle(
    durationMillis: Long,
    predicate: ((T) -> Boolean)? = null
): Flow<T> {
    var lastEmitTime = 0L
    return filter { value ->
        if (predicate != null && !predicate(value)) {
            // 不满足条件的值直接通过,不受节流限制
            true
        } else {
            // 满足条件的值应用节流逻辑
            val currentTime = System.currentTimeMillis()
            (currentTime - lastEmitTime > durationMillis).also { shouldEmit ->
                if (shouldEmit) {
                    lastEmitTime = currentTime
                }
            }
        }
    }
}

一、Flow 的基本结构

复制代码
┌─────────────┐      ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   Source    │─────▶│ Operator 1  │─────▶│ Operator 2  │─────▶│  Collector  │
│    Flow     │      │   (map)     │      │  (filter)   │      │  (collect)  │
└─────────────┘      └─────────────┘      └─────────────┘      └─────────────┘
     上游               中间操作符            中间操作符              下游

二、flow { } 构建器的原理

函数签名

kotlin 复制代码
fun <T> flow(block: suspend FlowCollector<T>.() -> Unit): Flow<T>

block 参数的作用

  • blockFlowCollector<T> 的扩展函数
  • 相当于重写了 collect 方法
  • 可以决定是否调用上游的 collect

责任链的关键

复制代码
                     是否调用上游 collect?
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
                 调用了               不调用
                    │                   │
            ┌───────┴───────┐          │
            ▼               ▼          ▼
      上游 Flow 执行    上游产生数据   上游不执行
      耗费资源          传递给下游     节省资源
                                    (责任链断开)

三、冷流 vs 热流对比

冷流实现(throttleFirst)

复制代码
时间线:  0ms      500ms     1200ms
         │         │         │
调用:    collect1  collect2  collect3
         │         │         │
判断:    ✓ 执行    ✗ 跳过    ✓ 执行
         │         │         │
上游:    执行      不执行     执行
         │                   │
结果:    发射数据             发射数据

代码流程:
┌─────────────────────────────────┐
│ flow {                          │
│   if (时间满足) {                │
│     this@throttleFirst.collect {│◀─ 只有条件满足时才调用
│       emit(it)                  │
│     }                           │
│   }                             │
│   // 条件不满足,不调用 collect   │◀─ 上游不执行
│ }                               │
└─────────────────────────────────┘

特点:
✓ 可以完全跳过上游执行
✓ 节省网络、数据库、计算等资源
✓ 适合按需执行的耗时操作

热流实现(throttle)

复制代码
时间线:  0ms    100ms   200ms   300ms   400ms
         │      │       │       │       │
上游:    ①      ②       ③       ④       ⑤
         │      │       │       │       │
判断:    ✓发射  ✗过滤   ✗过滤   ✓发射   ✗过滤
         │              │               │
下游:    ①              ④               

代码流程:
┌─────────────────────────────────┐
│ flow {                          │
│   this@throttle.collect { value│◀─ 始终调用上游 collect
│     if (时间满足) {              │
│       emit(value)               │◀─ 选择性发射
│     }                           │
│     // 上游持续执行,只是不发射   │
│   }                             │
│ }                               │
└─────────────────────────────────┘

特点:
✓ 上游持续执行
✓ 数据持续产生
✓ 只是选择性发射给下游
✓ 适合持续的数据流(事件、传感器等)

四、责任链的断开与延续

场景 1:责任链正常执行

复制代码
┌────────┐   collect   ┌────────┐   collect   ┌────────┐   collect   ┌────────┐
│ Source │────────────▶│  Map   │────────────▶│ Filter │────────────▶│Collect │
└────────┘             └────────┘             └────────┘             └────────┘
    │                      │                      │                      │
    ▼                      ▼                      ▼                      ▼
  执行                   执行                   执行                   接收
  发射数据               转换数据               过滤数据               处理数据

代码:
source.collect { value1 ->           // Collector 调用 source.collect
  emit(transform(value1))            // Source 发射 → Map 的 FlowCollector
}

map.collect { value2 ->              // Map 调用 filter.collect  
  if (predicate(value2)) {           // Filter 的 FlowCollector
    emit(value2)
  }
}

filter.collect { value3 ->           // Filter 调用 最终 collect
  handleData(value3)                 // 最终的处理逻辑
}

场景 2:责任链被拦截

复制代码
┌────────┐             ┌────────┐             ┌────────┐             ┌────────┐
│ Source │      ✗      │  Map   │      ✗      │ Filter │      ✗      │Collect │
└────────┘             └────────┘             └────────┘             └────────┘
    │                      │                      │                      │
    ▼                      ▼                      ▼                      ▼
  不执行                 不执行                 不执行                 无数据
  
                    ┌────────────┐
                    │ Intercept  │◀─ 拦截点:不调用上游 collect
                    └────────────┘
                          │
                          ▼
                     条件不满足
                    不调用 collect
                    责任链断开

代码:
flow {
  if (!condition) {
    // 不调用 this@intercept.collect
    // 上游 Source 不会执行
    return@flow
  }
  
  this@intercept.collect { value ->
    emit(value)
  }
}

五、FlowCollector 的角色

FlowCollector 是责任链的节点

复制代码
每个操作符创建一个新的 FlowCollector:

┌──────────────────────────────────────────────────────────────┐
│                      Flow 责任链                              │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────┐        ┌──────────────┐                  │
│  │ FlowCollector│        │ FlowCollector│                  │
│  │     #1       │        │     #2       │                  │
│  │              │        │              │                  │
│  │  collect {   │───────▶│  collect {   │                  │
│  │    emit(...)─┼───┐    │    emit(...)─┼───┐             │
│  │  }           │   │    │  }           │   │             │
│  └──────────────┘   │    └──────────────┘   │             │
│                     │                        │             │
│  Source Flow      接收                   Map Flow        接收  │
│                     │                        │             │
│                     └────────────────────────┘             │
│                          数据流动方向                       │
└──────────────────────────────────────────────────────────────┘

FlowCollector 的两个关键方法

kotlin 复制代码
interface FlowCollector<T> {
    // 发射数据给下游
    suspend fun emit(value: T)
}

interface Flow<T> {
    // 收集上游的数据
    suspend fun collect(collector: FlowCollector<T>)
}

数据流动过程

复制代码
1. 最终的 collect 被调用:
   flow.collect { value -> 
     println(value) 
   }

2. 触发最后一个操作符的 collect:
   filter.collect(FlowCollector { value ->
     println(value)
   })

3. filter 内部调用上游的 collect:
   this@filter.collect { value ->
     if (predicate(value)) {
       emit(value)  // 发送给下游的 FlowCollector
     }
   }

4. 依次向上,直到 Source Flow:
   sourceFlow.collect { value ->
     emit(value)  // 开始向下发送数据
   }

5. 数据从 Source 流向下游:
   Source.emit → Collector#1.emit → Collector#2.emit → ... → 最终 collect

六、性能优化总结

冷流优化

复制代码
场景:按钮快速点击 5 次,触发网络请求

不使用 throttleFirst:
┌─────┬─────┬─────┬─────┬─────┐
│Click│Click│Click│Click│Click│
└──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘
   ▼     ▼     ▼     ▼     ▼
 请求1  请求2  请求3  请求4  请求5
 (浪费了 4 个请求)

使用 throttleFirst(1000ms):
┌─────┬─────┬─────┬─────┬─────┐
│Click│Click│Click│Click│Click│
└──┬──┴─✗───┴─✗───┴─✗───┴──┬──┘
   ▼                        ▼
 请求1                    请求5
 (只发起 2 个请求,节省 3 个)

热流优化

复制代码
场景:位置每 100ms 更新一次,UI 只需 500ms 更新

不使用 throttle:
时间:  0   100  200  300  400  500  600
位置: ①───②───③───④───⑤───⑥───⑦
UI:   更新 更新 更新 更新 更新 更新 更新
     (频繁更新,性能浪费)

使用 throttle(500ms):
时间:  0   100  200  300  400  500  600
位置: ①───②───③───④───⑤───⑥───⑦
       │   ✗   ✗   ✗   ✗   │   ✗
UI:   更新                 更新
     (减少UI刷新,提升性能)

七、核心要点

1. 责任链的核心

复制代码
┌────────────────────────────────────────┐
│ 调用上游 collect?                      │
├────────────────────────────────────────┤
│                                        │
│  YES → 责任链延续                       │
│        上游执行,数据流动                 │
│        可以处理、转换、过滤数据           │
│                                        │
│  NO  → 责任链断开                       │
│        上游不执行,节省资源               │
│        适合条件拦截、节流                 │
│                                        │
└────────────────────────────────────────┘

2. 冷流 vs 热流

特性 冷流 (throttleFirst) 热流 (throttle)
上游执行 条件控制 始终执行
判断时机 调用 collect 前 emit 时
资源消耗 可以跳过 持续产生
适用场景 按需计算 持续数据流
典型例子 网络请求 UI 事件

3. FlowCollector 的作用

复制代码
┌─────────────────────────────────────┐
│ FlowCollector 是责任链的节点         │
├─────────────────────────────────────┤
│                                     │
│ 1. collect() - 从上游收集数据        │
│ 2. emit()    - 向下游发射数据        │
│ 3. 决定是否调用上游的 collect        │
│ 4. 决定是否调用下游的 emit           │
│                                     │
└─────────────────────────────────────┘

4. 使用建议

复制代码
┌──────────────┬────────────────────────┐
│   使用冷流    │        使用热流         │
├──────────────┼────────────────────────┤
│ ✓ 网络请求   │ ✓ UI 事件流            │
│ ✓ 数据库操作 │ ✓ 传感器数据           │
│ ✓ 文件 I/O   │ ✓ WebSocket 消息       │
│ ✓ 复杂计算   │ ✓ 实时数据更新         │
│ ✓ 按需执行   │ ✓ 持续数据源           │
└──────────────┴────────────────────────┘

总结

Flow 的责任链模式核心在于:

  1. flow { } 构建器的 block 参数决定是否调用上游 collect
  2. 冷流:条件不满足时不调用 collect,上游不执行,节省资源
  3. 热流:始终调用 collect,只在 emit 时判断,适合持续数据流
  4. 责任链断开:不调用上游 collect,整个上游链都不执行
  5. 责任链延续:调用上游 collect,数据在各个 FlowCollector 间流动
相关推荐
Zender Han2 小时前
Flutter 新版 Google Sign-In 插件完整解析(含示例讲解)
android·flutter·ios·web
来来走走5 小时前
Android开发(Kotlin) LiveData的基本了解
android·开发语言·kotlin
。puppy6 小时前
MySQL 远程登录实验:通过 IP 地址跨机器连接实战指南
android·adb
dongdeaiziji6 小时前
深入理解 Kotlin 中的构造方法
android·kotlin
风起云涌~7 小时前
【Android】浅谈Navigation
android
游戏开发爱好者87 小时前
iOS 商店上架全流程解析 从工程准备到审核通过的系统化实践指南
android·macos·ios·小程序·uni-app·cocoa·iphone
QuantumLeap丶9 小时前
《Flutter全栈开发实战指南:从零到高级》- 18 -自定义绘制与画布
android·flutter·ios
.豆鲨包9 小时前
【Android】 View事件分发机制源码分析
android·java
花落归零10 小时前
Android 小组件AppWidgetProvider的使用
android
弥巷10 小时前
【Android】常见滑动冲突场景及解决方案
android·java