Kotlin Flow 防抖(Debounce)、节流(Throttle)、去重(distinctUntilChanged) —— 新手指南

概念

防抖(Debounce) 的核心思想在事件触发后,等待一段时间,如果在这段时间内没有新的事件触发,才执行操作;如果有新事件,则重新计时。

节流(Throttle) 是控制事件频率的重要操作符,它确保在指定时间间隔内最多只处理一次事件。与防抖不同,节流是定期执行,而不是等待稳定。

去重(distinctUntilChanged) 是流处理中非常重要的操作,它确保只发射与上一个值不同的值,避免不必要的处理和更新。

一、防抖 (Debounce)

1. 核心概念

等待一段时间,期间没有新事件才执行

kotlin 复制代码
fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T>

2. 工作原理

text 复制代码
输入: A----B----------C---D--------E
时间: | 300ms |     | 300ms |
输出:       B               D       E

3. 代码示例

kotlin 复制代码
// 搜索框防抖
class SearchViewModel {
    private val _searchQuery = MutableStateFlow("")
    
    val searchResults = _searchQuery
        .debounce(300) // 用户停止输入300ms后才搜索
        .filter { it.length >= 2 }
        .flatMapLatest { query ->
            searchApi.search(query)
                .catch { emit(emptyList()) }
        }
        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
    
    fun onQueryChanged(query: String) {
        _searchQuery.value = query
    }
}

// 详细示例
fun main() = runBlocking {
    val flow = flow {
        emit("A")      // t=0
        delay(100)     // t=100
        emit("AB")     // t=100 (被取消)
        delay(200)     // t=300
        emit("ABC")    // t=300 (满足300ms条件)
        delay(100)     // t=400
        emit("ABCD")   // t=400 (被取消)
        delay(400)     // t=800
        emit("ABCDE")  // t=800 (满足300ms条件)
    }
    
    flow.debounce(300)
        .collect { println("防抖输出: $it") }
    // 输出: ABC, ABCDE
}

4. 适用场景

  • 搜索框输入
  • 窗口大小调整
  • 表单验证
  • 实时保存

二、节流 (Throttle)

1. 核心概念

固定时间内只执行一次

kotlin 复制代码
// Kotlin 没有内置的 throttle,但可以用其他方式实现
fun <T> Flow<T>.throttleFirst(timeWindow: Long): Flow<T> = channelFlow {
    var lastEmissionTime = 0L
    
    collect { value ->
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastEmissionTime > timeWindow) {
            send(value)
            lastEmissionTime = currentTime
        }
    }
}

// 或者使用 sample (Kotlin 1.7.0+)
// .sample(periodMillis)

2. 工作原理

text 复制代码
输入: A-B---C-D---E-F---G
时间: | 300ms | 300ms | 300ms |
输出: A       C       E       G

3. 代码示例

kotlin 复制代码
// 实现 throttleFirst
fun <T> Flow<T>.throttleFirst(periodMillis: Long): Flow<T> = flow {
    var lastTime = 0L
    collect { value ->
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastTime >= periodMillis) {
            emit(value)
            lastTime = currentTime
        }
    }
}

// 使用示例
fun main() = runBlocking {
    val flow = flow {
        repeat(10) {
            emit(it)
            delay(100) // 每100ms发射一个
        }
    }
    
    flow.throttleFirst(300) // 每300ms最多一个
        .collect { println("节流输出: $it") }
    // 输出: 0, 3, 6, 9
}

// 按钮防连点
class ButtonViewModel {
    private val _clicks = MutableSharedFlow<Unit>()
    
    val throttledClicks = _clicks
        .throttleFirst(1000) // 1秒内只能点击一次
        .onEach { performAction() }
    
    suspend fun onClick() = _clicks.emit(Unit)
}

4. 节流类型对比

kotlin 复制代码
// throttleFirst - 时间窗口内的第一个
// throttleLast - 时间窗口内的最后一个
fun <T> Flow<T>.throttleLast(periodMillis: Long): Flow<T> = flow {
    coroutineScope {
        val channel = produceIn(this)
        var lastValue: T? = null
        var hasValue = false
        
        launch {
            while (true) {
                delay(periodMillis)
                if (hasValue) {
                    emit(lastValue!!)
                    hasValue = false
                }
            }
        }
        
        for (value in channel) {
            lastValue = value
            hasValue = true
        }
    }
}

5. 适用场景

  • 按钮防连点
  • 滚动事件
  • 实时位置更新
  • 游戏循环

三、去重 (distinctUntilChanged)

1. 核心概念

只发射与上一次不同的值

kotlin 复制代码
fun <T> Flow<T>.distinctUntilChanged(): Flow<T>
fun <T, K> Flow<T>.distinctUntilChanged(selector: (T) -> K): Flow<T>

2. 工作原理

text 复制代码
输入: A A B B B C A A
输出: A   B     C A

3. 代码示例

kotlin 复制代码
// 基本使用
fun main() = runBlocking {
    val flow = flow {
        emit(1)
        emit(1)  // 被过滤
        emit(2)
        emit(2)  // 被过滤
        emit(1)  // 与前一个不同,发射
        emit(3)
    }
    
    flow.distinctUntilChanged()
        .collect { println("去重后: $it") }
    // 输出: 1, 2, 1, 3
}

// 自定义比较逻辑
data class User(val id: Int, val name: String)

fun main() = runBlocking {
    val usersFlow = flow {
        emit(User(1, "Alice"))
        emit(User(1, "Alice"))  // 相同ID,被过滤
        emit(User(2, "Bob"))
        emit(User(1, "Alice"))  // ID不同,发射
    }
    
    // 只根据ID去重
    usersFlow.distinctUntilChanged { old, new ->
        old.id == new.id
    }.collect { println("User: ${it.id} - ${it.name}") }
    // 输出: User(1,Alice), User(2,Bob), User(1,Alice)
    
    // 或者使用 keySelector
    usersFlow.distinctUntilChanged { it.id }
        .collect { println("User: ${it.id} - ${it.name}") }
}

4. 复杂对象去重

kotlin 复制代码
// 处理列表变化
val listFlow = flow {
    emit(listOf(1, 2, 3))
    emit(listOf(1, 2, 3))  // 相同内容,被过滤
    emit(listOf(1, 2))
    emit(listOf(1, 2, 3, 4))
}

// 比较列表内容
listFlow.distinctUntilChanged { old, new ->
    old == new
}

// 处理网络状态
sealed class NetworkState {
    object Loading : NetworkState()
    data class Success(val data: String) : NetworkState()
    data class Error(val message: String) : NetworkState()
}

val stateFlow = MutableStateFlow<NetworkState>(NetworkState.Loading)

// 防止重复的 Loading 状态
stateFlow.distinctUntilChanged()
    .collect { state ->
        when (state) {
            is NetworkState.Loading -> showLoading()
            is NetworkState.Success -> showData(state.data)
            is NetworkState.Error -> showError(state.message)
        }
    }

四、三者的对比与组合

1. 对比表

特性 防抖 (Debounce) 节流 (Throttle) 去重 (distinctUntilChanged)
目的 等待稳定 限制频率 避免重复
输出时间 停止后 固定间隔 值变化时
适用场景 输入结束处理 连续事件 状态更新
内存 需要计时器 需要计时器 只需缓存前值

2. 组合使用示例

kotlin 复制代码
// 搜索功能完整实现
class AdvancedSearchViewModel {
    private val _query = MutableStateFlow("")
    
    val searchResults = _query
        .filter { it.isNotBlank() }
        .debounce(300)          // 防抖:停止输入300ms后
        .distinctUntilChanged()  // 去重:查询变化才搜索
        .throttleFirst(1000)    // 节流:1秒内最多一次
        .flatMapLatest { query ->
            performSearch(query)
                .retry(2)       // 失败重试2次
                .catch { emit(SearchResult.Error(it)) }
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = SearchResult.Empty
        )
    
    private suspend fun performSearch(query: String): Flow<SearchResult> {
        return flow {
            emit(SearchResult.Loading)
            delay(500) // 模拟网络请求
            val results = repository.search(query)
            emit(SearchResult.Success(results))
        }
    }
}

// 实时数据监控
class SensorMonitor {
    private val _sensorData = MutableSharedFlow<SensorData>()
    
    val processedData = _sensorData
        .throttleFirst(100)     // 节流:100ms采样一次
        .filter { it.isValid() } // 过滤无效数据
        .distinctUntilChanged { old, new ->
            abs(old.value - new.value) < 0.01 // 变化小于0.01视为相同
        }
        .debounce(500)          // 稳定500ms后持久化
        .onEach { saveToDatabase(it) }
        .shareIn(
            scope = CoroutineScope(Dispatchers.Default),
            started = SharingStarted.Lazily
        )
}

3. 性能优化组合

kotlin 复制代码
// 处理高频事件
fun <T> Flow<T>.optimizeForUI(
    debounceTime: Long = 300,
    throttleTime: Long = 16, // ~60fps
    distinct: Boolean = true
): Flow<T> = flow {
    if (distinct) {
        distinctUntilChanged()
            .debounce(debounceTime)
            .throttleFirst(throttleTime)
            .collect { emit(it) }
    } else {
        debounce(debounceTime)
            .throttleFirst(throttleTime)
            .collect { emit(it) }
    }
}

// 使用
viewModel.dataFlow
    .optimizeForUI(debounceTime = 200, throttleTime = 32)
    .onEach { updateUI(it) }
    .launchIn(lifecycleScope)

五、实际应用场景

1. 搜索功能

kotlin 复制代码
searchQueryFlow
    .filter { it.length >= 3 }
    .debounce(300)        // 停止输入300ms
    .distinctUntilChanged() // 避免相同查询
    .flatMapLatest { query ->
        searchRepository.search(query)
    }

2. 实时聊天

kotlin 复制代码
messageFlow
    .throttleFirst(100)   // 防止消息轰炸
    .distinctUntilChanged { old, new ->
        old.id == new.id  // 避免重复消息
    }
    .onEach { displayMessage(it) }

3. 表单验证

kotlin 复制代码
formInputFlow
    .debounce(500)        // 输入完成500ms后验证
    .distinctUntilChanged() // 值变化才验证
    .map { validate(it) }
    .onEach { showValidationResult(it) }

4. 位置跟踪

kotlin 复制代码
locationFlow
    .throttleFirst(1000)  // 1秒更新一次
    .filter { it.accuracy < 50 } // 精度过滤
    .distinctUntilChanged { old, new ->
        distanceBetween(old, new) < 10 // 移动超过10米
    }
    .onEach { updateMap(it) }

六、注意事项

1. 执行上下文

kotlin 复制代码
// 正确:在适当的上下文中使用
flow
    .debounce(300)
    .flowOn(Dispatchers.Default)  // 防抖操作在后台线程
    .collectOn(Dispatchers.Main)  // 结果在主线程收集

// 避免在主线程进行长时间防抖

2. 取消处理

kotlin 复制代码
flow
    .debounce(300)
    .onCompletion { println("Flow completed") }
    .catch { println("Error: $it") }
    .collect { /* ... */ }

3. 内存管理

kotlin 复制代码
// SharedFlow 自动管理
val sharedFlow = someFlow
    .debounce(300)
    .shareIn(scope, replay = 1)

// StateFlow 保持最新值
val stateFlow = someFlow
    .distinctUntilChanged()
    .stateIn(scope, SharingStarted.Lazily, initialValue)

总结

操作符 最佳实践 常见错误
debounce 搜索输入、保存操作 时间设置过短/过长
throttle 滚动事件、按钮点击 与 debounce 混淆
distinctUntilChanged 状态更新、数据同步 忘记自定义比较器

合理组合这三个操作符,可以创建高效、响应迅速的用户体验,同时减少不必要的计算和网络请求。

相关推荐
AI视觉网奇2 小时前
android yolo12 android 实战笔记
android·笔记·yolo
海上飞猪2 小时前
【Mysql】Mysql的安装部署和使用
android·mysql·adb
我是好小孩2 小时前
【Android】项目的组件化搭建
android
aqi003 小时前
FFmpeg开发笔记(九十四)基于Kotlin的国产开源推拉流框架anyRTC
android·ffmpeg·kotlin·音视频·直播·流媒体
马 孔 多 在下雨3 小时前
Android 组件化开发基础实践
android
技术摆渡人3 小时前
Android 系统技术探索(2)构建大脑(System Services & PMS)
android
tealcwu3 小时前
【Unity实战】如何使用VS Code在真实Android设备上调试 Unity应用
android·unity·游戏引擎
鹏多多3 小时前
flutter-屏幕自适应插件flutter_screenutil教程全指南
android·前端·flutter
小龙报3 小时前
【C语言初阶】动态内存分配实战指南:C 语言 4 大函数使用 + 经典笔试题 + 柔性数组优势与内存区域
android·c语言·开发语言·数据结构·c++·算法·visual studio