【Kotlin】 数据流完全指南:冷流、热流与 Android 实战

文章目录

    • 一、数据流简介
      • [1.1 Kotlin 数据流概述](#1.1 Kotlin 数据流概述)
      • [1.2 核心特性](#1.2 核心特性)
      • [1.3 Flow 的基本组件](#1.3 Flow 的基本组件)
    • 二、数据流的使用方法
      • [2.1 正向流](#2.1 正向流)
        • [2.1.1 创建数据流](#2.1.1 创建数据流)
        • [2.1.2 修改数据流](#2.1.2 修改数据流)
        • [2.1.3 收集数据流](#2.1.3 收集数据流)
      • [2.2 反向流](#2.2 反向流)
        • [2.2.1 创建数据流](#2.2.1 创建数据流)
        • [2.2.2 修改并收集数据流](#2.2.2 修改并收集数据流)
      • [2.3 数据流在 Jetpack 中的应用场景](#2.3 数据流在 Jetpack 中的应用场景)
    • 三、数据流的行为模式
      • [3.1 冷流(Cold Flow)](#3.1 冷流(Cold Flow))
        • [3.1.1 什么是冷流](#3.1.1 什么是冷流)
        • [3.1.2 为什么 Kotlin 默认使用冷流](#3.1.2 为什么 Kotlin 默认使用冷流)
      • [3.2 热流(Hot Flow)](#3.2 热流(Hot Flow))
        • [3.2.1 什么是热流](#3.2.1 什么是热流)
        • [3.2.2 Kotlin 如何支持热流](#3.2.2 Kotlin 如何支持热流)
      • [3.3 使用哪种数据流](#3.3 使用哪种数据流)
        • [3.3.1 如何选择](#3.3.1 如何选择)
        • [3.3.2 冷流转热流:shareIn 与 stateIn](#3.3.2 冷流转热流:shareIn 与 stateIn)
    • 参考资料

一、数据流简介

1.1 Kotlin 数据流概述

在 Kotlin 协程中,普通挂起函数只能返回单个值,调用一次即结束。但实际开发中往往需要处理持续产生的多值序列------如实时数据、用户输入、WebSocket 消息等。Kotlin 数据流(Flow)正是为此而生。

Flow 是基于协程的异步流处理 API,支持按顺序、异步地发射多个同类型值 。其核心价值在于:用声明式、近乎同步的代码风格处理异步事件序列,同时享受协程的结构化并发与资源安全优势。

它类似于 RxJava 的 Observable,但完全基于协程设计,可与协程无缝集成。

1.2 核心特性

  • 声明式 & 可组合 :通过 mapfilterzip 等链式操作,以近乎同步的代码风格处理异步事件序列。
  • 异步非阻塞emitcollect 均为挂起函数,可与协程无缝组合,后台计算不阻塞主线程。
  • 结构化并发:收集在协程作用域内运行,作用域取消时(如界面关闭)自动停止,避免内存泄漏。
  • 背压支持 :生产者过快时,可通过缓冲、conflate 等机制处理速度不匹配,而回调无法做到。
  • 冷流(Cold Stream) :默认模式,仅在 collect 时执行,每次收集都从头发射。详见 [3.1 冷流](#3.1 冷流)。

1.3 Flow 的基本组件

Flow 的基本组件有:

  • Flow :数据流接口,通过 flow { ... } 构建器定义。
  • 发射器(Emitter) :在构建器内调用 emit() 发射数据。
  • 收集器(Collector) :通过 collect() 接收数据,持续消费直到流结束或协程取消。

发射与收集均为挂起函数,以异步方式生成和消费值。示例:

kotlin 复制代码
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

// 使用 Flow 构建器创建冷数据流
fun simpleFlow(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100) // 模拟异步操作
        emit(i)    // 发射数据
    }
}

fun main() = runBlocking {
    
    // 收集数据
    simpleFlow().collect { value -> 
        println(value) // 依次打印 1, 2, 3
    }
}

二、数据流的使用方法

数据流包含三个角色:

  • 提供方:生成数据,可借助协程异步生产。
  • 中介(可选):修改流中的值或流本身。
  • 使用方:消费流中的值。

(图源:Android 官方文档

在 Android 中常见两种流向:

  • 正向流 :数据层(Repository)→ 领域层/ViewModel(map 转换)→ 界面层(收集并更新 UI)。
  • 反向流:界面层(用户输入)→ ViewModel(响应事件流)→ 更新界面状态。

2.1 正向流

2.1.1 创建数据流

使用 flow { } 构建器创建流,在内部通过 emit 发射值。下例中,数据源以固定间隔轮询资讯,作为提供方:

kotlin 复制代码
class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
    suspend fun fetchLatestNews(): List<ArticleHeadline>
}

注意flow 在协程内执行,发射有序(挂起函数返回后才 emit 下一个值)。不可在 withContext 或新协程中调用 emit,否则需用 callbackFlow

2.1.2 修改数据流

中介通过中间运算符 (如 mapfilter)链式修改流,这些操作是惰性的,仅在收集时执行。Repository 示例:

kotlin 复制代码
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val userData: UserData
) {
    /**
     * Returns the favorite latest news applying transformations on the flow.
     * These operations are lazy and don't trigger the flow. They just transform
     * the current value emitted by the flow at that point in time.
     */
    val favoriteLatestNews: Flow<List<ArticleHeadline>> =
        newsRemoteDataSource.latestNews
            // Intermediate operation to filter the list of favorite topics
            .map { news -> news.filter { userData.isFavoriteTopic(it) } }
            // Intermediate operation to save the latest news in the cache
            .onEach { news -> saveInCache(news) }
}
2.1.3 收集数据流

终端运算符 collect 触发流执行并消费所有发射值。ViewModel 示例:

kotlin 复制代码
class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            // Trigger the flow and consume its elements using collect
            newsRepository.favoriteLatestNews.collect { favoriteNews ->
                // Update View with the latest favorite news
            }
        }
    }
}

收集数据流会触发提供方刷新最新资讯,并以固定时间间隔发出网络请求。由于提供方始终通过 while(true) 循环保持活跃状态,因此,在清除 ViewModel 并取消 viewModelScope 数据流后,数据流将关闭。

2.2 反向流

在反向流中,界面层是提供方。使用 callbackFlow 将回调式 API(如 TextWatcher)转为 Flow,并通过 awaitClose 在流取消时清理监听:

2.2.1 创建数据流
kotlin 复制代码
// --- 界面层(作为 提供方)---
// 创建一个表示搜索框输入的 Flow
val searchFlow: Flow<String> = callbackFlow {
    val watcher = object : TextWatcher {
        override fun onTextChanged(text: String) {
            trySend(text) // 将每次输入作为一个事件"发射"到流中
        }
    }
    editText.addTextChangedListener(watcher)
    
    // 当流被取消时(如界面销毁),移除监听,避免内存泄漏
    awaitClose { editText.removeTextChangedListener(watcher) }
}

// 将这个流"暴露"给 ViewModel
viewModel.bindSearchFlow(searchFlow)
2.2.2 修改并收集数据流

ViewModel 对输入流做防抖、过滤、去重,再触发搜索并收集结果:

kotlin 复制代码
// --- ViewModel层(作为 中介/使用方)---
fun bindSearchFlow(searchFlow: Flow<String>) {
    viewModelScope.launch {
        searchFlow
            .debounce(300) // 中介操作1:防抖,等用户停止输入300ms
            .filter { it.length >= 2 } // 中介操作2:过滤,只搜索至少2个字符
            .distinctUntilChanged() // 中介操作3:去重,避免连续相同搜索
            .flatMapLatest { query -> // 中介操作4:切换到最新搜索,取消之前的
                repository.search(query) // 触发真正的搜索(返回一个 Flow<Result>)
            }
            .collect { result -> // 使用方:消费最终的搜索结果
                _searchResults.value = result // 更新 LiveData/StateFlow
            }
    }
}

2.3 数据流在 Jetpack 中的应用场景

Jetpack 已广泛支持 Flow。以 Room 为例,DAO 返回 Flow 即可监听数据库变更:

kotlin 复制代码
@Dao
abstract class ExampleDao {
    @Query("SELECT * FROM Example")
    abstract fun getExamples(): Flow<List<Example>>
}

表数据变更时,Flow 自动发射最新列表。

三、数据流的行为模式

冷流像点播 :你点才开始播,每人从头看。热流像直播:不管你看不看都在播,中途进入只能从当前看起。

3.1 冷流(Cold Flow)

3.1.1 什么是冷流

冷流类似于私人定制,具备如下特征:

  • 按需启动 :仅当使用方调用终端操作符(如 collect)时,提供方(flow 构建器内的代码)才会开始执行。
  • 独立副本 :每次调用 collect,都会获得完整、从头开始的值序列。
  • 无共享状态:不同收集者互不共享进度。

适用场景:网络请求、数据库查询等需独立数据源,或每个订阅者需从头消费完整数据。

3.1.2 为什么 Kotlin 默认使用冷流

Kotlin Flow 默认使用冷流,这主要是为了安全可预测

  1. 结构化并发友好:冷流的生命周期与收集它的协程完全绑定。协程取消,流的生产就停止。这完美契合了协程的结构化并发理念,避免了资源泄漏。
  2. 幂等性 :每次收集都获得一份完整的、独立的副本,行为可预测。这对于网络请求、数据库查询、文件读取等场景至关重要。你希望每次查询都获取最新数据,而不是错过已经开始的数据。
  3. 资源管理简单:由于是独立的,不需要复杂的"多播"或"连接"管理。

3.2 热流(Hot Flow)

3.2.1 什么是热流

热流类似于直播,具备如下特征:

  • 主动发射:不管有无订阅者,数据都会产生。
  • 共享数据源:多订阅者共享同一份数据流,但只能收到订阅后的数据。

适用场景:IM 消息、定位更新、全局状态等需多订阅者共享的实时数据。

3.2.2 Kotlin 如何支持热流

Kotlin 通过 StateFlowSharedFlow 提供热流:

特性 StateFlow SharedFlow
设计语义 状态容器 事件流
初始值 必须提供 可选,通过 replay 控制
重放机制 固定重放 1 个值 可配置 replay = 0, 1, ...
去重策略 自动去重(equals 相同则不发射) 不去重,每次 emit 都触发
当前值访问 value 属性 无,需通过缓存或收集获取
背压处理 固定为 Conflated(只取最新) 可配置:SUSPENDDROP_OLDESTDROP_LATEST
典型场景 UI 状态 一次性事件(Toast、导航)、实时消息流

(1) SharedFlow

举个例子,您可以使用 SharedFlow向应用程序的其他部分发送"滴答"信号,以便所有内容能够定期、同时刷新。

kotlin 复制代码
// 集中管理应用程序内容何时需要刷新的类
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // 后备属性,避免从其他类发出数据流
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
    val tickFlow: SharedFlow<Unit> = _tickFlow

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit) // 发出信号
                delay(tickIntervalMs) // 延迟指定间隔
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler,
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // 监听滴答信号更新
            tickHandler.tickFlow.collect {
                refreshLatestNews() // 收集到信号时,刷新新闻
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

(2) StateFlow

StateFlow 是一个特殊的 SharedFlow,专门为管理"状态"而设计。其内部维护了一个可变的状态,并在状态发生变化时通知所有观察者。

其典型案例是MVVM中保存界面的状态的 UiState。

kotlin 复制代码
class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Update View with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

3.3 使用哪种数据流

3.3.1 如何选择
  • 冷流:数据需按需计算、一次性、独立。
  • 热流:数据需多观察者共享、长期存活、活跃状态或事件。
3.3.2 冷流转热流:shareIn 与 stateIn

在 Android 开发中,一个常见的模式是:数据层用冷流(Flow)暴露数据。对于需要在多个观察者间共享的数据(特别是昂贵的查询结果),可以在数据层使用 shareIn转换为热流 SharedFlow以优化性能。对于 UI 状态,则在 ViewModel 层使用 stateIn(或直接管理 MutableStateFlow)将其转换为热流 StateFlow,供 UI 安全地观察,并与 ViewModel 的生命周期绑定。

举个例子,一个从远程(如 Firestore)或本地获取数据的 Flow冷流 。如果多个 ViewModel 或屏幕都需要这个数据,使用 shareIn可以确保数据源只执行一次,然后将结果广播给所有订阅者,避免了重复的网络请求或数据库查询,从而显著提升应用性能和效率。

kotlin 复制代码
// 在 DataSource/Repository 层
class NewsRepository(
    private val newsApi: NewsApi,
    private val externalScope: CoroutineScope // 通常是 applicationScope
) {
    val topHeadlines: Flow<List<Article>> = flow {
        emit(newsApi.fetchTopHeadlines()) // 网络请求
    }.shareIn(
        scope = externalScope,
        started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), // 有订阅者时才启动,无人订阅5秒后停止
        replay = 1 // 新订阅者立即得到最新缓存
    )
}

参考资料

Android 上的 Kotlin 数据流 | Android Developers

相关推荐
聆风吟º1 小时前
【C标准库】C语言memset函数详解:从原理到实战避坑
c语言·开发语言·库函数·memset
有位神秘人1 小时前
Android中Mvvm+Retrofit的常用网络架构记录
android·网络·retrofit
快快起来写代码1 小时前
反射可能用于的场景
开发语言·python
Ivanqhz2 小时前
图着色寄存器分配算法(Graph Coloring)
开发语言·javascript·python·算法·蓝桥杯·rust
一直都在5722 小时前
JAVA类的加载过程
java·开发语言
我命由我123452 小时前
Element Plus 问题:选择框表单校验没有触发
开发语言·前端·javascript·html·ecmascript·html5·js
常利兵2 小时前
Android 字体字重设置:从XML到Kotlin的奇妙之旅
android·xml·kotlin
iPadiPhone2 小时前
性能之基:Java IO 体系深度解析、面试陷阱与实战指南
java·开发语言·后端·面试
于先生吖2 小时前
前后端分离开发 Java 跑腿系统:用户 + 骑手 + 后台三端实战
java·开发语言