banner
NEWS LETTER

Android Compose 状态管理:State、ViewModel、Flow 与副作用

Scroll down

一、为什么 Compose 更强调状态

传统 View 体系里,页面通常是“找到控件,然后修改控件属性”:

1
2
textView.text = user.name
progressBar.visibility = View.GONE

Compose 的思路更接近“状态是什么,界面就长什么样”。当状态变化时,相关 Composable 会重新执行,生成新的 UI 描述。

简单理解:

  • 状态是数据源。
  • UI 是状态的展示结果。
  • 事件负责修改状态。
  • Compose 根据状态变化自动刷新界面。

这种模型能减少手动同步 UI 的代码,但前提是状态边界要设计清楚。

二、局部状态:remember 和 mutableStateOf

只在当前组件内部使用的状态,可以放在 Composable 中:

1
2
3
4
5
6
7
8
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }

Button(onClick = { count++ }) {
Text(text = "count: $count")
}
}

remember 会在重组时保留对象,mutableStateOf 会让 Compose 观察这个值。

适合放在局部状态里的内容:

  • 输入框临时内容。
  • 展开或收起状态。
  • Tab 选中项。
  • 弹窗是否显示。

不适合放在局部状态里的内容:

  • 页面核心业务数据。
  • 需要跨页面共享的数据。
  • 网络请求结果。
  • 进程重建后必须恢复的数据。

三、状态提升

当一个组件既展示状态又修改状态时,它会变得不容易复用。更常见的方式是把状态放到父级,由子组件只负责展示和回调。

1
2
3
4
5
6
7
8
9
10
11
@Composable
fun SearchBar(
keyword: String,
onKeywordChange: (String) -> Unit
) {
TextField(
value = keyword,
onValueChange = onKeywordChange,
placeholder = { Text("搜索") }
)
}

父级负责保存状态:

1
2
3
4
5
6
7
8
9
@Composable
fun SearchScreen() {
var keyword by remember { mutableStateOf("") }

SearchBar(
keyword = keyword,
onKeywordChange = { keyword = it }
)
}

状态提升的好处:

  • 子组件更容易复用。
  • 状态变化路径更清晰。
  • 测试时更容易传入不同状态。
  • 页面复杂后更容易迁移到 ViewModel。

四、页面状态放到 ViewModel

页面级业务状态建议放进 ViewModel,例如加载中、列表数据、错误信息和空页面状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
data class ArticleUiState(
val loading: Boolean = false,
val items: List<Article> = emptyList(),
val errorMessage: String? = null
)

class ArticleViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ArticleUiState())
val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()

fun loadArticles() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(loading = true)
runCatching {
repository.getArticles()
}.onSuccess { list ->
_uiState.value = ArticleUiState(items = list)
}.onFailure { e ->
_uiState.value = ArticleUiState(errorMessage = e.message)
}
}
}
}

Composable 只订阅状态并展示:

1
2
3
4
5
6
7
8
9
@Composable
fun ArticleRoute(viewModel: ArticleViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()

ArticleScreen(
uiState = uiState,
onRetry = viewModel::loadArticles
)
}

这样页面逻辑不会散落在多个 Composable 里。

五、副作用怎么放

Compose 中不要在 Composable 函数体里直接做网络请求、写数据库或启动协程。因为 Composable 可能会频繁重组,直接执行副作用很容易重复触发。

常用副作用 API:

  • LaunchedEffect:进入组合或 key 变化时启动协程。
  • DisposableEffect:注册和释放监听器。
  • SideEffect:每次成功重组后执行同步副作用。
  • rememberUpdatedState:在长生命周期回调中拿到最新参数。

页面首次加载可以这样写:

1
2
3
4
5
6
7
8
9
@Composable
fun ArticleRoute(viewModel: ArticleViewModel = viewModel()) {
LaunchedEffect(Unit) {
viewModel.loadArticles()
}

val uiState by viewModel.uiState.collectAsState()
ArticleScreen(uiState = uiState)
}

如果 key 改变才需要重新加载,就把 key 放进 LaunchedEffect

1
2
3
LaunchedEffect(categoryId) {
viewModel.loadByCategory(categoryId)
}

六、列表性能注意点

LazyColumn 展示列表时建议提供稳定 key:

1
2
3
4
5
6
7
8
LazyColumn {
items(
items = uiState.items,
key = { it.id }
) { article ->
ArticleItem(article = article)
}
}

这样数据插入、删除、排序时,Compose 更容易复用已有节点,减少状态错乱和不必要的重组。

同时要避免在 item 内创建昂贵对象:

1
val formatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") }

能用 remember 缓存的对象,不要每次重组都重新创建。

七、常见问题

7.1 状态放太散

一个页面里如果多个组件都各自保存业务状态,后期会很难判断数据来源。页面核心状态应该集中到 ViewModel,再向下分发。

7.2 用普通变量保存状态

普通变量变化不会触发 Compose 刷新:

1
var name = ""

需要使用 mutableStateOfStateFlow 或其他 Compose 可观察状态。

7.3 在 Composable 里直接请求网络

这种写法容易因为重组重复请求。网络请求应放到 ViewModel,再通过副作用触发。

八、小结

Compose 状态管理可以按层次拆分:

  • 组件内部临时状态:remember
  • 组件复用状态:状态提升。
  • 页面业务状态:ViewModel + StateFlow
  • 一次性动作和异步任务:副作用 API。

核心原则是让数据流单向流动:状态向下传递,事件向上传递。页面复杂之后,这个原则比具体 API 更重要。

其他文章
目录导航 置顶
  1. 1. 一、为什么 Compose 更强调状态
  2. 2. 二、局部状态:remember 和 mutableStateOf
  3. 3. 三、状态提升
  4. 4. 四、页面状态放到 ViewModel
  5. 5. 五、副作用怎么放
  6. 6. 六、列表性能注意点
  7. 7. 七、常见问题
    1. 7.1. 7.1 状态放太散
    2. 7.2. 7.2 用普通变量保存状态
    3. 7.3. 7.3 在 Composable 里直接请求网络
  8. 8. 八、小结
请输入关键词进行搜索