在ViewModel中存储LazyListState引用是否会引发内存泄漏?
问题:Compose中ViewModel持有UI回调是否会引发内存泄漏?
我用Compose的Pull Refresh实现下拉刷新的LazyColumn,希望刷新后滚动到列表顶部。目前的做法是在ViewModel里设置一个onRefreshFinished回调,在loadList执行完后触发滚动到顶部的操作。
对应的Compose代码:
@Composable fun pullRefreshList( contacts: List<String>, navController: NavController, modifier: Modifier = Modifier, ) { val viewModel = viewModel<MainViewModel>() val refreshing by viewModel.isRefreshing.collectAsState() val listState = rememberLazyListState() viewModel.onRefreshFinished = { // Scroll to the top when the callback is invoked listState.scrollToItem(0) } val pullRefreshState = rememberPullRefreshState(refreshing, viewModel::loadList) Box(Modifier.pullRefresh(pullRefreshState)) { LazyColumn(state = listState, modifier = modifier) { items(contacts) { contactName -> Row( modifier = Modifier .height(30.dp) .fillMaxWidth() .clickable { navController.navigate("screen2") }, horizontalArrangement = Arrangement.Center, ) { Text(text = contactName, fontSize = 24.sp) } } } PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } }
对应的ViewModel代码:
class MainViewModel: ViewModel() { private val _isRefreshing = MutableStateFlow(false) val isRefreshing = _isRefreshing.asStateFlow() private var nextInt = 0 var onRefreshFinished: (suspend () -> Unit)? = null fun loadList() { viewModelScope.launch { _isRefreshing.value = true addToList() _isRefreshing.value = false onRefreshFinished?.invoke() } } }
在基于XML的UI中,我们通常不会让ViewModel持有View引用,因为这会导致内存泄漏。想请教在Compose中,这种做法是否也会引发内存泄漏?
回答
这种做法确实存在内存泄漏风险,本质和XML时代的问题一致:
- ViewModel的生命周期远长于Composable(ViewModel会在配置变更后存活,而Composable会随页面销毁、重组被回收)。如果ViewModel持有指向Composable内部状态(比如
LazyListState)的回调,当Composable被销毁后,这个回调引用会阻止相关UI对象被GC回收,进而引发内存泄漏。
推荐的安全实现方式
我们可以在Composable侧监听刷新状态变化,当刷新完成时自动触发滚动,完全不需要让ViewModel绑定UI回调:
@Composable fun pullRefreshList( contacts: List<String>, navController: NavController, modifier: Modifier = Modifier, ) { val viewModel = viewModel<MainViewModel>() val refreshing by viewModel.isRefreshing.collectAsState() val listState = rememberLazyListState() val pullRefreshState = rememberPullRefreshState(refreshing, viewModel::loadList) // 监听刷新状态,从true变为false时(刷新完成)滚动到顶部 LaunchedEffect(refreshing) { if (!refreshing) { listState.scrollToItem(0) } } Box(Modifier.pullRefresh(pullRefreshState)) { LazyColumn(state = listState, modifier = modifier) { items(contacts) { contactName -> Row( modifier = Modifier .height(30.dp) .fillMaxWidth() .clickable { navController.navigate("screen2") }, horizontalArrangement = Arrangement.Center, ) { Text(text = contactName, fontSize = 24.sp) } } } PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter)) } }
对应的ViewModel可以去掉回调,保持业务职责单一:
class MainViewModel: ViewModel() { private val _isRefreshing = MutableStateFlow(false) val isRefreshing = _isRefreshing.asStateFlow() private var nextInt = 0 fun loadList() { viewModelScope.launch { _isRefreshing.value = true addToList() _isRefreshing.value = false } } }
这种方式更安全的原因
LaunchedEffect会跟随Composable的生命周期自动管理:当Composable被销毁时,LaunchedEffect会自动取消,不会持有多余引用。- ViewModel只负责业务逻辑(加载数据、维护刷新状态),不涉及任何UI操作或引用,完全符合MVVM的职责分离原则。
内容的提问来源于stack exchange,提问作者Hiroku
相关产品推荐
相关产品推荐

