/ Android  Jetpack Compose  状态管理  ViewModel  StateFlow  remember  UI开发  Kotlin 

Jetpack Compose 状态管理最佳实践:从 remember 到 ViewModel 全解析


封面

为什么状态管理是 Compose 的核心挑战

Jetpack Compose 采用声明式 UI 模型,界面由状态驱动。当状态变化时,Compose 会自动重组(Recompose)受影响的 Composable。这种模式极大简化了 UI 逻辑,但也带来新的挑战:如果状态设计不合理,会导致不必要的重组,拖累渲染性能,甚至引发难以追踪的 Bug。

理解 Compose 状态管理,需要掌握三个核心问题:

  • 状态放在哪里? Composable 内部 vs ViewModel vs 全局

  • 如何触发最小范围的重组? 避免无效刷新

  • 如何与生命周期安全协作? 防止内存泄漏和 crash

remember 与 rememberSaveable 的正确用法

remember 是 Compose 状态的基础原语,它在重组时保留值,但不跨越 Activity 重建。rememberSaveable 则借助 Bundle 机制,在配置变更(如旋转屏幕)后恢复状态。

常见误区:把复杂对象直接放入 rememberSaveable,导致 Bundle 超限。正确做法是只保存能重建 UI 的最小数据(如选中索引、滚动位置),而非完整数据对象。

// ✅ 正确:只保存轻量索引
var selectedTab by rememberSaveable { mutableStateOf(0) }

// ❌ 错误:序列化整个列表
var items by rememberSaveable { mutableStateOf(listOf<Item>()) }

对于不可序列化的对象,可以实现 Saver 接口,或直接交给 ViewModel 管理。

State 提升:让 Composable 保持无状态

State 提升(State Hoisting)是 Compose 官方推荐的模式:将状态从子 Composable 移到父级,子组件只接收值和回调,自身不持有状态。这样做的好处是:

  • 子组件变为无状态(Stateless),更容易测试和复用

  • 单一数据源,逻辑集中,减少状态不同步问题

  • 父组件可精确控制哪些状态变化触发哪些子组件重组

// 无状态子组件
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit
) {
    OutlinedTextField(
        value = query,
        onValueChange = onQueryChange,
        label = { Text("搜索") }
    )
}

// 父组件持有状态
@Composable
fun SearchScreen() {
    var query by remember { mutableStateOf("") }
    SearchBar(query = query, onQueryChange = { query = it })
}

ViewModel + StateFlow 的标准协作模式

对于跨越 Composable 层级的业务状态,应交由 ViewModel 统一管理,通过 StateFlow 暴露给 UI 层。Compose 侧使用 collectAsStateWithLifecycle()(来自 lifecycle-runtime-compose)安全订阅,避免后台状态更新导致的资源浪费。

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

    fun loadArticles() {
        viewModelScope.launch {
            val articles = repository.getArticles()
            _uiState.update { it.copy(articles = articles, isLoading = false) }
        }
    }
}

// Composable
@Composable
fun ArticleScreen(viewModel: ArticleViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    if (uiState.isLoading) {
        CircularProgressIndicator()
    } else {
        LazyColumn {
            items(uiState.articles) { article ->
                ArticleItem(article)
            }
        }
    }
}

将 UI 状态封装成单一的 data class(UiState),比多个独立 StateFlow 更易维护,也减少了多次 collect 带来的重组次数。

性能优化:减少不必要的重组

重组是 Compose 的核心机制,但频繁重组会降低帧率。以下是几个实战技巧:

  • 使用 key() 稳定列表项:LazyColumn 中为每个 item 指定稳定的 key,避免数据变化时全量重建。

  • 拆分读取状态的 Composable:将访问高频变化状态的代码下沉到尽可能小的 Composable,限制重组范围。

  • 使用 derivedStateOf 派生计算值:对依赖多个状态的计算结果,用 derivedStateOf 包裹,只有依赖项真正变化时才更新。

// derivedStateOf 示例:只有过滤结果真正变化时才重组
val filteredList by remember {
    derivedStateOf {
        allItems.filter { it.title.contains(searchQuery) }
    }
}

合理运用这些技巧,可以让 Compose 应用在复杂交互场景下依然保持 60fps 流畅渲染。

发布评论

热门评论区: