一、为什么 Compose 更强调状态
传统 View 体系里,页面通常是“找到控件,然后修改控件属性”:
1 | textView.text = user.name |
Compose 的思路更接近“状态是什么,界面就长什么样”。当状态变化时,相关 Composable 会重新执行,生成新的 UI 描述。
简单理解:
- 状态是数据源。
- UI 是状态的展示结果。
- 事件负责修改状态。
- Compose 根据状态变化自动刷新界面。
这种模型能减少手动同步 UI 的代码,但前提是状态边界要设计清楚。
二、局部状态:remember 和 mutableStateOf
只在当前组件内部使用的状态,可以放在 Composable 中:
1 |
|
remember 会在重组时保留对象,mutableStateOf 会让 Compose 观察这个值。
适合放在局部状态里的内容:
- 输入框临时内容。
- 展开或收起状态。
- Tab 选中项。
- 弹窗是否显示。
不适合放在局部状态里的内容:
- 页面核心业务数据。
- 需要跨页面共享的数据。
- 网络请求结果。
- 进程重建后必须恢复的数据。
三、状态提升
当一个组件既展示状态又修改状态时,它会变得不容易复用。更常见的方式是把状态放到父级,由子组件只负责展示和回调。
1 |
|
父级负责保存状态:
1 |
|
状态提升的好处:
- 子组件更容易复用。
- 状态变化路径更清晰。
- 测试时更容易传入不同状态。
- 页面复杂后更容易迁移到 ViewModel。
四、页面状态放到 ViewModel
页面级业务状态建议放进 ViewModel,例如加载中、列表数据、错误信息和空页面状态。
1 | data class ArticleUiState( |
Composable 只订阅状态并展示:
1 |
|
这样页面逻辑不会散落在多个 Composable 里。
五、副作用怎么放
Compose 中不要在 Composable 函数体里直接做网络请求、写数据库或启动协程。因为 Composable 可能会频繁重组,直接执行副作用很容易重复触发。
常用副作用 API:
LaunchedEffect:进入组合或 key 变化时启动协程。DisposableEffect:注册和释放监听器。SideEffect:每次成功重组后执行同步副作用。rememberUpdatedState:在长生命周期回调中拿到最新参数。
页面首次加载可以这样写:
1 |
|
如果 key 改变才需要重新加载,就把 key 放进 LaunchedEffect:
1 | LaunchedEffect(categoryId) { |
六、列表性能注意点
LazyColumn 展示列表时建议提供稳定 key:
1 | LazyColumn { |
这样数据插入、删除、排序时,Compose 更容易复用已有节点,减少状态错乱和不必要的重组。
同时要避免在 item 内创建昂贵对象:
1 | val formatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") } |
能用 remember 缓存的对象,不要每次重组都重新创建。
七、常见问题
7.1 状态放太散
一个页面里如果多个组件都各自保存业务状态,后期会很难判断数据来源。页面核心状态应该集中到 ViewModel,再向下分发。
7.2 用普通变量保存状态
普通变量变化不会触发 Compose 刷新:
1 | var name = "" |
需要使用 mutableStateOf、StateFlow 或其他 Compose 可观察状态。
7.3 在 Composable 里直接请求网络
这种写法容易因为重组重复请求。网络请求应放到 ViewModel,再通过副作用触发。
八、小结
Compose 状态管理可以按层次拆分:
- 组件内部临时状态:
remember。 - 组件复用状态:状态提升。
- 页面业务状态:ViewModel +
StateFlow。 - 一次性动作和异步任务:副作用 API。
核心原则是让数据流单向流动:状态向下传递,事件向上传递。页面复杂之后,这个原则比具体 API 更重要。
- 本文链接: https://blog.hansong.icu/2026/06/22/Android_Compose_State/
- 版权声明: 本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 4.0 许可协议。