Compose 副作用(Side Effects)等API 详解
官网Compose 中的附带效应 | Jetpack Compose | Android Developers前置概念什么是副作用Compose 的核心理念是声明式 UI 重组Recomposition。重组可能随时发生、可能部分执行、可能被取消。因此任何「修改 Compose 状态之外的世界」的操作网络请求、订阅、日志、回调等都属于副作用必须放在合适的 API 中托管否则会出现泄漏、重复执行、读到过期数据等问题。官方把副作用 API 分成两大类一次性副作用LaunchedEffect、DisposableEffect、SideEffect状态转换副作用produceState、derivedStateOf、snapshotFlow辅助工具rememberCoroutineScope、rememberUpdatedState1. LaunchedEffect作用在组合Composition中启动一个协程当key变化或组合离开时自动取消协程。适用场景进入组合时执行一次性异步任务加载数据、动画、跳转且希望在重组时不重复执行。kotlinComposable 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它只在进入组合时触发。kotlinComposable 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。kotlinComposable 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。可带 keykey 变化时先清理旧资源再重新初始化。适用场景需要配对「注册/注销」「订阅/退订」「添加监听/移除监听」的资源。kotlinComposable 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。kotlinComposable fun AnalyticsScreen(pageName: String) { SideEffect { // 每次重组成功后上报不是进入组合时才上报 Analytics.log(screen_shown, pageName) } }与 LaunchedEffect 对比LaunchedEffect进入组合/key 变化时跑一次面向「事件」。SideEffect每次重组后跑一次面向「状态同步」。注意SideEffect 中不要做重活/异步它跑在主线程组合提交阶段应保持轻量。6. produceState作用把一个非 Compose 状态源异步回调、Flow、网络请求转换成 Compose 的StateT并托管其生命周期。返回值一个State可以直接用于 UI。kotlinComposable fun produceUserData(userId: String): StateUser? { return produceStateUser?(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 状态派生成一个新的状态且只有结果真正变化时才触发重组缓存计算 精确通知。适用场景昂贵或高频的状态计算避免每次源状态变化都连锁重组。kotlinComposable fun LazyColumnWithButton(list: ListItem) { 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 方向相反derivedStateOfState → State留在 Compose 世界snapshotFlowState → Flow流向协程/Flow 世界kotlinComposable 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把异步装进 StatederivedStateOf状态转换源状态变化State → State结果变了才重组计算缓存snapshotFlow状态转换源状态变化State → Flow把状态接进 Flow 管道选型口诀进来做一次异步任务 →LaunchedEffect点击/回调里启动协程 →rememberCoroutineScope长协程要用最新参数 →rememberUpdatedState注册/订阅要配对清理 →DisposableEffect每次重组后同步给外部 →SideEffect异步数据要变成 UI 状态 →produceState状态推导、避免连锁重组 →derivedStateOf状态要用 Flow 操作符处理 →snapshotFlow