Compose 状态管理完全指南:从 remember 到 StateFlow

2024-04-05 · 22 min · 状态管理

状态管理是 Compose 应用的核心。理解何时使用 remembermutableStateOfrememberSaveableStateFlow,是写出健壮 Compose 代码的关键。

一、Compose 中的状态是什么?

在 Compose 中,状态是随时间变化并驱动 UI 更新的数据。当状态变化时,Compose 会自动重组(Recomposition)读取该状态的 Composable。

@Composable
fun Counter() {
    // count 是状态,变化时 UI 自动更新
    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Clicked $count times")
    }
}

二、状态 API 速查表

API 作用 生命周期
mutableStateOf 创建可观察的状态 需配合 remember
remember 在重组间保持值 Composable 在组合中存在期间
rememberSaveable 保存到 Bundle 跨配置变更、进程死亡
StateFlow ViewModel 中的状态流 ViewModel 生命周期
collectAsState 将 Flow 转为 Compose State 订阅期间

三、remember vs rememberSaveable

// remember:配置变更(如旋转屏幕)后会丢失
var count by remember { mutableStateOf(0) }

// rememberSaveable:配置变更后保留
var count by rememberSaveable { mutableStateOf(0) }

💡 何时用 rememberSaveable?

用户输入的表单数据、滚动位置、展开/折叠状态等需要跨配置变更保留的 UI 状态。

自定义 Saver

对于复杂对象,需要提供自定义 Saver:

data class City(val name: String, val country: String)

val CitySaver = Saver<City, Bundle>(
    save = { city ->
        Bundle().apply {
            putString("name", city.name)
            putString("country", city.country)
        }
    },
    restore = { bundle ->
        City(
            bundle.getString("name") ?: "",
            bundle.getString("country") ?: ""
        )
    }
)

@Composable
fun CityPicker() {
    var selectedCity by rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Beijing", "China"))
    }
}

四、状态提升(State Hoisting)

状态提升是 Compose 的核心模式:将状态移到调用方,让 Composable 变成无状态的纯函数。

// ❌ 有状态组件:难以复用和测试
@Composable
fun ExpandableCard() {
    var expanded by remember { mutableStateOf(false) }
    Card(onClick = { expanded = !expanded }) { ... }
}

// ✅ 无状态组件:状态由调用方控制
@Composable
fun ExpandableCard(
    expanded: Boolean,
    onExpandChange: (Boolean) -> Unit
) {
    Card(onClick = { onExpandChange(!expanded) }) { ... }
}

// 调用方控制状态
@Composable
fun CardList() {
    var expandedIndex by remember { mutableStateOf(-1) }

    items.forEachIndexed { index, item ->
        ExpandableCard(
            expanded = index == expandedIndex,
            onExpandChange = { isExpanded ->
                expandedIndex = if (isExpanded) index else -1
            }
        )
    }
}

五、ViewModel 与 StateFlow

对于屏幕级状态和业务逻辑,使用 ViewModel + StateFlow:

class ProfileViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(ProfileUiState())
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

    fun loadProfile() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }

            val profile = repository.getProfile()

            _uiState.update {
                it.copy(isLoading = false, profile = profile)
            }
        }
    }
}

data class ProfileUiState(
    val isLoading: Boolean = false,
    val profile: Profile? = null,
    val error: String? = null
)

@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    when {
        uiState.isLoading -> LoadingIndicator()
        uiState.error != null -> ErrorMessage(uiState.error)
        uiState.profile != null -> ProfileContent(uiState.profile)
    }
}

六、状态分层:UI State vs Domain State

类型 示例 存放位置
UI 元素状态 TextField 输入、动画进度 remember / rememberSaveable
屏幕 UI 状态 加载状态、列表数据、错误信息 ViewModel + StateFlow
业务/领域状态 用户登录状态、购物车 Repository / DataStore

七、处理副作用

状态变化时需要执行副作用(如网络请求、日志),使用 Effect API:

@Composable
fun UserProfile(userId: String) {
    var user by remember { mutableStateOf<User?>(null) }

    // userId 变化时重新加载
    LaunchedEffect(userId) {
        user = repository.getUser(userId)
    }

    user?.let { UserContent(it) }
}

常用 Effect API

八、常见陷阱与解决方案

1. 忘记使用 remember

// ❌ 每次重组都创建新状态,点击无效
@Composable
fun BrokenCounter() {
    var count = mutableStateOf(0)  // 缺少 remember!
    Button(onClick = { count.value++ }) {
        Text("${count.value}")
    }
}

// ✅ 正确
var count by remember { mutableStateOf(0) }

2. 在 remember 中使用不稳定的 key

// ❌ 每次重组 listOf 都是新对象
val items = remember(listOf(1, 2, 3)) { ... }

// ✅ 使用稳定的 key 或无 key
val items = remember { listOf(1, 2, 3) }

3. collectAsState 在错误位置

// ❌ 在 LazyColumn item 中 collect,可能导致问题
LazyColumn {
    items(ids) { id ->
        val item by viewModel.getItemFlow(id).collectAsState(null)
    }
}

// ✅ 在外层 collect,传递数据给 item
val items by viewModel.itemsFlow.collectAsState()
LazyColumn {
    items(items) { item -> ItemRow(item) }
}

九、状态管理最佳实践

总结

Compose 状态管理的核心原则: