news 2026/10/18 4:47:35

Compose 副作用(Side Effects)等API 详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Compose 副作用(Side Effects)等API 详解

官网:Compose 中的附带效应 | Jetpack Compose | Android Developers

前置概念:什么是副作用?

Compose 的核心理念是声明式 UI + 重组(Recomposition)。重组可能随时发生、可能部分执行、可能被取消。因此,任何「修改 Compose 状态之外的世界」的操作(网络请求、订阅、日志、回调等)都属于副作用,必须放在合适的 API 中托管,否则会出现泄漏、重复执行、读到过期数据等问题。

官方把副作用 API 分成两大类:

  • 一次性副作用:LaunchedEffect、DisposableEffect、SideEffect

  • 状态转换副作用:produceState、derivedStateOf、snapshotFlow

  • 辅助工具:rememberCoroutineScope、rememberUpdatedState


1. LaunchedEffect

作用:在组合(Composition)中启动一个协程,当key变化或组合离开时自动取消协程。

适用场景:进入组合时执行一次性异步任务(加载数据、动画、跳转),且希望在重组时不重复执行。

kotlin

@Composable fun HomeScreen(viewModel: HomeViewModel) { LaunchedEffect(Unit) { // Unit 表示只执行一次 viewModel.loadData() } LaunchedEffect(viewModel.userId) { // userId 变化时重新执行,旧的协程被取消 val detail = fetchUserDetail(viewModel.userId) viewModel.onDetailLoaded(detail) } }

要点:

  • key相等 → 不重启;key变化 → 取消旧协程、启动新协程;离开组合 → 自动取消。

  • 常见错误:用viewModel当 key 导致每次重组重启协程;或在该协程里做不该随状态变化重做的重活。


2. rememberCoroutineScope

作用:获取一个与组合生命周期绑定的CoroutineScope,允许你在回调中(而非组合体内)手动启动协程。

适用场景:事件回调触发的异步操作,如按钮点击刷新、滚动到底部加载更多。此时不能用LaunchedEffect(它只在进入组合时触发)。

kotlin

@Composable fun RefreshButton(viewModel: HomeViewModel) { val scope = rememberCoroutineScope() Button(onClick = { scope.launch { viewModel.refresh() // 例如弹一个 Snackbar,需要 Compose 作用域 // snackbarHostState.showSnackbar("已刷新") } }) { Text("刷新") } }

与 LaunchedEffect 的区别:

触发时机取消时机
LaunchedEffect进入组合 / key 变化自动触发离开组合自动取消
rememberCoroutineScope手动launch触发离开组合自动取消

要点:不要remember { CoroutineScope(...) },那样离开组合不会取消,会造成泄漏;必须用rememberCoroutineScope。


3. rememberUpdatedState

作用:保存对某个最新状态的引用,使得长时间运行的副作用协程里读到的是最新的值,而不是启动时捕获的旧值。

解决问题:LaunchedEffect(Unit)的协程只启动一次,里面直接捕获的变量是旧的。用rememberUpdatedState包装后,协程每次读取都是新值。

经典例子:屏幕上有倒计时,期间用户修改了timeout,协程应使用最新的timeout。

kotlin

@Composable fun TimeoutEffect( timeout: Long, onTimeout: () -> Unit ) { val currentOnTimeout by rememberUpdatedState(onTimeout) val currentTimeout by rememberUpdatedState(timeout) LaunchedEffect(Unit) { delay(currentTimeout) // 每次读取都是调用方传入的最新值 currentOnTimeout() // 回调也是最新的 } }

调用方不断传新回调时,副作用仍然用最新的回调执行——避免捕获旧的 lambda 造成 bug。

一句话:rememberUpdatedState把「值快照」变成「值的引用快照」。


4. DisposableEffect

作用:进入组合执行一次初始化,离开组合时执行清理(onDispose)。可带 key,key 变化时先清理旧资源再重新初始化。

适用场景:需要配对「注册/注销」「订阅/退订」「添加监听/移除监听」的资源。

kotlin

@Composable fun BackHandlerEffect(onBack: () -> Unit) { val currentOnBack by rememberUpdatedState(onBack) DisposableEffect(Unit) { val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() = currentOnBack() } backDispatcher.addCallback(callback) onDispose { callback.remove() // 离开组合时移除监听,防止泄漏 } } }

要点:

  • 必须调用onDispose(代码里必须出现它),否则编译期有 lint 检查提醒。

  • onDispose中不能读组合里的状态(组合已开始卸载);要把需要的最新状态用rememberUpdatedState捕获后传进去(如上例)。


5. SideEffect

作用:每次成功重组后都会执行,把 Compose 状态同步到非 Compose 世界。

适用场景:Compose → 外部系统的单向同步:Analytics 上报、日志、把重组后的值写入 Android View。

kotlin

@Composable fun AnalyticsScreen(pageName: String) { SideEffect { // 每次重组成功后上报;不是进入组合时才上报 Analytics.log("screen_shown", pageName) } }

与 LaunchedEffect 对比:

  • LaunchedEffect:进入组合/key 变化时跑一次(面向「事件」)。

  • SideEffect:每次重组后跑一次(面向「状态同步」)。

注意:SideEffect 中不要做重活/异步,它跑在主线程组合提交阶段,应保持轻量。


6. produceState

作用:把一个非 Compose 状态源(异步回调、Flow、网络请求)转换成 Compose 的State<T>,并托管其生命周期。

返回值:一个State,可以直接用于 UI。

kotlin

@Composable fun produceUserData(userId: String): State<User?> { return produceState<User?>(initialValue = null, userId) { val data = repository.fetchUser(userId) // 异步加载 value = data // 赋值即更新 UI // 也可以收集 Flow: // repository.userFlow(userId).collect { value = it } } }

要点:

  • value的初始值由initialValue指定。

  • key(这里是userId)变化 → 取消旧的 producer 协程,重启。

  • 离开组合 → 自动取消。适合替代 ViewModel 中很多样板代码(简单场景)。

  • 组合体内不能有网络请求?produceState内部的协程是合法的,因为状态转换是被托管的。


7. derivedStateOf

作用:把一个或多个 Compose 状态派生成一个新的状态,且只有结果真正变化时才触发重组(缓存计算 + 精确通知)。

适用场景:昂贵或高频的状态计算,避免每次源状态变化都连锁重组。

kotlin

@Composable fun LazyColumnWithButton(list: List<Item>) { val listState = rememberLazyListState() // 只有「是否显示按钮」这个结果变化时才重组,而不是每滚动一像素就重组 val showBackToTop by remember { derivedStateOf { listState.firstVisibleItemIndex > 5 } } AnimatedVisibility(visible = showBackToTop) { FloatingActionButton(onClick = { scope.launch { listState.animateScrollToItem(0) } }) { Icon(Icons.Default.ArrowUpward, null) } } }

为什么不用普通计算?

kotlin

// ❌ 错误:每次重组都执行,且状态对象每次新建,失去缓存意义 val show = listState.firstVisibleItemIndex > 5 // ❌ 错误:普通 remember 返回普通 Boolean,首次计算后不再更新 val show = remember { listState.firstVisibleItemIndex > 5 } // ✅ derivedStateOf:状态变化→重新计算→只有结果变了才通知重组

要点:remember { derivedStateOf { ... } }内读的 State 被订阅;常用于滚动位置、文本是否超长、筛选计数等。


8. snapshotFlow

作用:把 Compose 的State 转换成 Flow,以便在协程里使用 Flow 的操作符(debounce、filter、distinctUntilChanged等)。

与 derivedStateOf 方向相反:

  • derivedStateOf:State → State(留在 Compose 世界)

  • snapshotFlow:State → Flow(流向协程/Flow 世界)

kotlin

@Composable fun SearchBar(viewModel: SearchViewModel) { val query = remember { mutableStateOf("") } LaunchedEffect(Unit) { snapshotFlow { query.value } // 把 Compose 状态转成 Flow .debounce(300) // 300ms 防抖 .distinctUntilChanged() .filter { it.isNotBlank() } .collect { q -> viewModel.search(q) } } TextField(value = query.value, onValueChange = { query.value = it }) }

要点:

  • snapshotFlow块内读的 State 变化时发送新值到 Flow。

  • 必须在协程中收集(常配合LaunchedEffect)。

  • 典型场景:搜索防抖、输入校验、表单状态上报。


汇总对比表

API类型触发时机核心用途一句话记忆
LaunchedEffect一次性进入组合 / key 变化启动托管协程进组合干活
rememberCoroutineScope工具手动 launch回调里启动协程点击等事件里干活
rememberUpdatedState工具重组即更新引用让旧协程读到新值给旧协程"换血"
DisposableEffect一次性进入组合 / key 变化初始化 + 清理资源记得打扫现场
SideEffect一次性每次重组后状态同步到外部汇报最新状态
produceState状态转换key 变化 / 收集源外部异步 → State把异步装进 State
derivedStateOf状态转换源状态变化State → State,结果变了才重组计算缓存
snapshotFlow状态转换源状态变化State → Flow把状态接进 Flow 管道

选型口诀

  • 进来做一次异步任务 →LaunchedEffect

  • 点击/回调里启动协程 →rememberCoroutineScope

  • 长协程要用最新参数 →rememberUpdatedState

  • 注册/订阅要配对清理 →DisposableEffect

  • 每次重组后同步给外部 →SideEffect

  • 异步数据要变成 UI 状态 →produceState

  • 状态推导、避免连锁重组 →derivedStateOf

  • 状态要用 Flow 操作符处理 →snapshotFlow

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 4:44:40

IDA花指令对抗实战:从反汇编原理到脚本化清理

开头做逆向这块的朋友&#xff0c;不管是分析恶意样本还是自己调试商业软件&#xff0c;迟早都会撞上花指令这堵墙。我第一次在 IDA 里碰到花指令时&#xff0c;反汇编窗口里一片飘红&#xff0c;F5 出来的伪代码完全没法看&#xff0c;函数边界被切得支离破碎&#xff0c;光是…

作者头像 李华
网站建设 2026/10/18 4:44:07

Codex 入门实践笔记:从安装到安全避坑

一、Codex 是什么OpenAI Codex 是 OpenAI 推出的 AI 编程 Agent&#xff0c;2026 年已全面升级&#xff0c;提供本地 CLI、VS Code 扩展、Cursor/Windsurf 集成和桌面应用四种使用形态。核心能力是在终端中自主完成代码编写、修改、执行 Shell 命令、搜索文件和抓取网页等完整工…

作者头像 李华
网站建设 2026/10/18 4:43:21

软件开发合同避坑指南:从条款拆解到自动化排期与验收

简介&#xff1a;这份软件开发合同文档面向需要签订或参考软件委托开发协议的甲方客户、乙方开发团队及法务人员&#xff0c;用于规范会员积分制刷卡消费管理系统、公司宣传网站、微信公共服务平台、店铺零售结算系统等多模块项目的合作流程。资源包内含1个doc文件&#xff0c;…

作者头像 李华
网站建设 2026/10/18 4:37:57

智能体设计方法论:从PEAS模型到ReAct架构的工程实践

最近几年&#xff0c;做大模型应用的人越来越绕不开一个词&#xff1a;智能体&#xff08;Agent&#xff09;。我见过不少团队&#xff0c;LLM API接得贼溜&#xff0c;RAG流程也跑得通&#xff0c;但一问到“你的智能体是怎么设计的”&#xff0c;就支支吾吾说不出个所以然。问…

作者头像 李华
网站建设 2026/10/18 4:37:29

恶意代码检测平台实战:从需求分析到TensorFlow部署避坑指南

简介&#xff1a;资源包内含一套完整的本科毕业设计恶意代码检测分类平台项目&#xff0c;面向网络安全方向的高校毕业生和入门开发者&#xff0c;覆盖恶意样本收集、特征提取、自动分类与报告生成等核心环节&#xff0c;并提供了需求分析、用户角色划分、性能指标以及TensorFl…

作者头像 李华
网站建设 2026/10/18 4:37:09

VC6 GDI横版过关游戏源码解析:整图加载与碰撞检测实现

简介&#xff1a;基于Visual C与GDI实现的横版过关游戏完整源码&#xff0c;玩法与画面参考经典超级玛丽&#xff0c;面向毕业设计、项目实训等场景&#xff0c;适合需要掌握C面向对象特性&#xff08;类、继承、多态&#xff09;与Windows GDI图形编程的在校学生。资源共32个文…

作者头像 李华