核心内容摘要
探秘“撸撸社”:触及心灵深处的创意乐园
探索 Flow 在 Android 开发中的应用在 Android 开发转向 Kotlin 优先的浪潮中Flow 作为 Kotlin 原生的响应式数据流方案以简洁、易懂、无额外依赖的优势成为解决异步数据处理问题的利器。
对于初学者而言掌握 Flow 不仅能告别回调地狱还能轻松实现 “数据驱动 UI”为后续学习 MVVM 架构、Jetpack 组件打下基础。
本文全程聚焦 Flow 核心技术用通俗的语言、详细的注释和实用的场景带初学者从入门到实战吃透 Flow 的应用逻辑。
核心认知Flow 到底是什么Flow 可以通俗理解为 可暂停的数据流管道 它能像水管输送水一样持续发射数据比如网络请求结果、数据库变化、用户输入而收集者比如 UI就像水龙头只有打开开始收集数据流才会流动发射数据关闭则停止这就是 Flow 的核心特性冷流Cold Stream。
3 个核心特点懒加载无收集者时Flow 不会发射任何数据节省资源协程友好支持挂起函数如delay不会阻塞主线程避免 ANR响应式数据变化时自动通知收集者无需手动刷新 UI。
核心优势为什么要学 Flow告别回调地狱用线性代码替代嵌套回调比如网络请求 数据处理 UI 更新一行链式调用搞定逻辑清晰线程调度超简单通过flowOn一句话指定数据发射的线程如 IO 线程处理网络请求无需复杂的线程池配置操作符 “开箱即用”自带过滤、转换、防抖等操作符不用自己写复杂逻辑生命周期安全结合 Kotlin 协程作用域如lifecycleScope页面销毁时自动取消数据流避免内存泄漏。
基础实战3 步上手 Flow 核心用法Flow 的核心流程只有 3 步创建 Flow发射数据→ 处理 Flow操作符→ 收集 Flow接收数据只需掌握这 3 步就能应对大部分简单场景。
第一步创建 Flow—— 让数据 “流起来”用flow构建器创建数据流通过emit()方法发射数据支持挂起函数比如delay模拟耗时操作。
// 示例1发射
的数字每次延迟1秒模拟耗时操作 val numberFlow flow { // flow构建器定义数据流的发射逻辑 for (i in
.
{ delay(
// 挂起函数不会阻塞主线程类似Thread.sleep但更友好 emit(i) // 发射数据把i放入数据流中 } }.flowOn(Dispatchers.IO) // 指定发射数据的线程IO线程适合处理网络、数据库等耗时操作第二步收集 Flow—— 接收数据并更新 UIFlow 的数据必须在协程作用域如lifecycleScopeActivity/Fragment 的生命周期协程viewModelScopeViewModel 的协程中收集用collect()方法接收数据// Activity中收集Flow class MainActivity : AppCompatActivity() { private lateinit var tvNumber: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvNumber findViewById(R.id.tv_number) // 用lifecycleScope启动协程自动跟随Activity生命周期页面销毁时取消 lifecycleScope.launch { // 收集numberFlow的数据 numberFlow.collect { number - // 这里运行在主线程可直接更新UI tvNumber.text 收到数据$number } } } }第三步操作符 —— 让数据 “变有用”Flow 提供了很多实用的操作符用于过滤、转换、限制数据流初学者先掌握 3 个最常用的filter过滤、map转换、debounce防抖用链式调用组合使用。
// 示例2对numberFlow进行处理只保留偶数并翻倍 val processedFlow numberFlow .filter { it % 2 0 } // 过滤只保留偶数1→滤除2→保留3→滤除... .map { it * 2 } // 转换把保留的偶数翻倍2→44→8 .debounce(
// 防抖300ms内无新数据才处理避免快速变化时频繁更新 // 收集处理后的数据流 lifecycleScope.launch { processedFlow.collect { result - tvNumber.text 处理后的数据$result // 输出
8 } }
实战场景用 Flow 实现实时搜索联想为了让初学者感受到 Flow 的实用价值我们实现一个简化版的 “实时搜索联想”—— 用户在输入框输入关键词Flow 自动处理防抖、过滤无效输入然后模拟网络请求返回联想结果全程聚焦 Flow 的数据流处理。
场景需求监听 EditText 的输入变化转为数据流过滤空输入、重复输入避免无效请求防抖 300ms用户快速输入时只在停止输入后发起请求模拟网络请求返回联想结果在 TextView 中展示结果处理加载状态。
完整实现
布局准备LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp !-- 搜索输入框 -- EditText android:idid/et_search android:layout_widthmatch_parent android:layout_heightwrap_content android:hint输入搜索关键词/ !-- 展示结果加载状态/联想结果/错误信息 -- TextView android:idid/tv_result android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop16dp android:textSize16sp/ /LinearLayout
代码实现class SearchActivity : AppCompatActivity() { private lateinit var etSearch: EditText private lateinit var tvResult: TextView //
第一步创建输入数据流监听EditText输入 private val searchQueryFlow: FlowString by lazy { // callbackFlow用于将回调转为Flow适合监听EditText、Button点击等 callbackFlow { // 监听输入变化 val textWatcher object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} // 输入结束后发射输入内容 override fun afterTextChanged(s: Editable?) { val query s?.toString() ?: // trySend非挂起函数用于在回调中发射数据 trySend(query) } } // 给EditText添加监听 etSearch.addTextChangedListener(textWatcher) // 取消回调页面销毁时移除监听避免内存泄漏 awaitClose { etSearch.removeTextChangedListener(textWatcher) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_search) etSearch findViewById(R.id.et_search) tvResult findViewById(R.id.tv_result) //
第二步处理数据流收集结果 lifecycleScope.launch { // 仅在页面可见RESUMED状态时收集避免后台浪费资源 repeatOnLifecycle(Lifecycle.State.RESUMED) { searchQueryFlow // 操作符1防抖300ms用户停止输入300ms后才处理 .debounce(
// 操作符2过滤空输入关键词为空或仅空格时不发起请求 .filter { query - query.isNotBlank() } // 操作符3过滤重复输入和上一次关键词相同不重复请求 .distinctUntilChanged() // 操作符4切换关键词时取消上一次未完成的请求避免结果错乱 .flatMapLatest { query - // 发射加载状态 tvResult.text 加载中... // 模拟网络请求返回Flow fetchSuggestionsFlow(query) } // 操作符5捕获异常比如网络错误 .catch { e - tvResult.text 出错了${e.message} } // 收集最终结果更新UI .collect { suggestions - tvResult.text 联想结果\n${suggestions.joinToString(\n)} } } } } //
模拟网络请求根据关键词返回联想结果封装为Flow private fun fetchSuggestionsFlow(query: String): FlowListString flow { // 模拟网络耗时1秒 delay(
// 模拟返回3条联想结果 val suggestions listOf( 「$query」的热门搜索, 相关推荐${query}教程, 大家都在搜${query}最新版 ) // 发射结果 emit(suggestions) }.flowOn(Dispatchers.IO) // 网络请求在IO线程执行不卡主线程 }关键说明callbackFlow专门用于将传统回调如 TextWatcher转为 Flow是处理 “监听类场景” 的常用工具debounce(
解决 “用户快速输入导致频繁请求” 的问题是搜索、筛选场景的必备操作符flatMapLatest关键词切换时会取消上一次的网络请求只保留最新请求的结果避免出现 “先输入 A后输入 B却先返回 A 的结果” 的错乱repeatOnLifecycle确保只有页面可见时才收集数据是 Flow 生命周期安全的核心一定要养成使用习惯。
关键扩展热流StateFlow/SharedFlow—— 应对更复杂场景前面学的 Flow 是 “冷流”适合一次性数据流如单次网络请求、输入监听。
但如果需要 “多地方共享数据”如 UI 状态、全局事件就需要用到热流—— 热流像 “广播”只要有数据发射所有订阅的收集者都能收到且不依赖收集者是否存在。
StateFlowUI 状态管理替代 LiveDataStateFlow 是 “有状态的热流”核心特点持有最新数据新收集者会立即收到当前值适合管理 UI 状态如加载中、成功、失败、用户信息。
// 示例ViewModel中用StateFlow管理用户状态 class UserViewModel : ViewModel() { // 私有可变StateFlow可修改必须设置初始值 private val _userState MutableStateFlowUserUiState(UserUiState.Loading) // 公开只读StateFlow外部只能收集不能修改 val userState: StateFlowUserUiState _userState.asStateFlow() // 模拟加载用户信息 fun loadUserInfo() { viewModelScope.launch { try { _userState.value UserUiState.Loading // 发射加载状态 delay(
// 模拟网络请求 val user User(小明,
// 模拟用户数据 _userState.value UserUiState.Success(user) // 发射成功状态 } catch (e: Exception) { _userState.value UserUiState.Error(e.message ?: 加载失败) // 发射错误状态 } } } // UI状态密封类初学者可理解为“固定类型的状态容器” sealed class UserUiState { object Loading : UserUiState() // 加载中 data class Success(val user: User) : UserUiState() // 成功携带数据 data class Error(val message: String) : UserUiState() // 失败携带错误信息 } // 用户数据类 data class User(val name: String, val age: Int) } // Activity中收集StateFlow class UserActivity : AppCompatActivity() { private val viewModel by viewModelsUserViewModel() private lateinit var tvUser: TextView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user) tvUser findViewById(R.id.tv_user) // 收集StateFlow生命周期安全 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel.userState.collect { state - // 根据状态更新UI when (state) { is UserViewModel.UserUiState.Loading - tvUser.text 加载中... is UserViewModel.UserUiState.Success - tvUser.text 姓名${state.user.name}年龄${state.user.age} is UserViewModel.UserUiState.Error - tvUser.text state.message } } } } // 触发加载用户信息 findViewByIdButton(R.id.btn_load).setOnClickListener { viewModel.loadUserInfo() } } }
SharedFlow事件通知替代 EventBusSharedFlow 是 “无状态的热流”核心特点不持有数据只发射新事件适合传递一次性事件如 Toast 提示、页面跳转、弹窗通知。
// 示例ViewModel中用SharedFlow发送通知事件 class MainViewModel : ViewModel() { // 配置replay1新收集者能收到最近1次事件避免遗漏 private val _event MutableSharedFlowString(replay
// 公开只读SharedFlow val event: SharedFlowString _event.asSharedFlow() // 发送事件如登录成功后 fun sendLoginSuccessEvent() { viewModelScope.launch { _event.emit(登录成功欢迎回来~) // 发射事件 } } } // Activity中收集事件 class MainActivity : AppCompatActivity() { private val viewModel by viewModelsMainViewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 收集SharedFlow事件 lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel.event.collect { message - Toast.makeText(thisMainActivity, message, Toast.LENGTH_SHORT).show() } } } // 点击按钮触发事件 findViewByIdButton(R.id.btn_login).setOnClickListener { viewModel.sendLoginSuccessEvent() } } }
4 个
常见问题与解决方案
忘记在协程中收集 Flow导致没数据问题直接调用flow.collect()报错 “suspend function call in non-coroutine context”解决方案Flow 的collect()是挂起函数必须在协程作用域中调用如lifecycleScope.launch、viewModelScope.launch。
冷流每次收集都重新执行导致重复请求问题多次调用collect()每次都会重新触发 Flow比如重复发起网络请求解决方案用shareIn将冷流转为热流让多个收集者共享同一数据流val sharedFlow fetchSuggestionsFlow(query) .shareIn( scope viewModelScope, // 协程作用域 started SharingStarted.WhileSubscribed(
, // 5秒内无收集者则停止 replay 1 // 保留最近1次结果 )
StateFlow 没设置初始值报错问题创建MutableStateFlow时没传初始值编译报错解决方案StateFlow 必须设置初始值如MutableStateFlow(UserUiState.Loading)因为它要 “持有最新状态”。
收集时没关联生命周期后台仍在运行问题页面退到后台Flow 还在收集数据如继续网络请求浪费资源解决方案所有收集都用repeatOnLifecycle(Lifecycle.State.RESUMED)包裹页面后台时自动暂停收集。
六、
总结Flow 的
核心价值与学习建议对于初学者而言Flow 的
核心价值在于 “用简单的方式处理复杂的异步数据流”—— 无需记忆复杂的框架 API仅通过 “创建 - 处理 - 收集” 三步就能解决用户输入、网络请求、状态管理等常见场景。
Flow 作为 Kotlin 协程生态的核心是 Android 开发的 “必备技能”。
掌握它后你会发现异步数据处理变得简单、直观后续学习 Jetpack 组件如 Room、ViewModel时也能更轻松地实现 “数据驱动 UI”。
作者陈晟原文链接探索 Flow 在 Android 开发中的应用