如何避免LazyColumn/LazyRow中Composable重复重组与ViewModel重复初始化?
一、避免ViewModel重复初始化与API重复调用
当前问题核心:LazyColumn的Item滚出屏幕时会被Compose销毁,滚回时重新创建。你在MyComposable内部用remember { component.viewModel }保存ViewModel,其作用域仅绑定当前Item的生命周期,因此Item重建时ViewModel会被重新实例化,导致init中的API请求重复执行。
解决方案1:将ViewModel生命周期提升至LazyColumn父级
把ViewModel的创建移到LazyColumn所在的父Composable中,通过参数传递给子Item,让ViewModel的生命周期与父级绑定,不受Item销毁重建影响:
// 父级Composable @Composable fun ParentListComposable() { val component = rememberComposableDaggerComponent() // 父级持有ViewModel实例 val viewModel = remember { component.viewModel } LazyColumn { item { MyComposable(viewModel = viewModel) } // 其他列表项... } } // 修改后的子Composable @Composable fun MyComposable( modifier: Modifier = Modifier, viewModel: MyViewModel ) { val uiState by viewModel.uiState.collectAsState() // 原有的UI逻辑 when (uiState) { is MyObjectState.Data -> { Surface(modifier = modifier) { // 数据展示UI } } is MyObjectState.Error, MyObjectState.Empty -> { Box(modifier = Modifier.height(0.dp)) } } }
解决方案2:ViewModel内部控制API仅执行一次
如果必须在Item内部创建ViewModel,可以在ViewModel中添加标记,确保API请求仅触发一次:
class MyViewModel(...) : ViewModel() { private val _uiState = MutableStateFlow<MyObjectState>(MyObjectState.Empty) override val uiState: StateFlow<MyObjectState> = _uiState.asStateFlow() private var hasInitiatedRequest = false init { if (!hasInitiatedRequest) { getFoo() hasInitiatedRequest = true } } private fun getFoo() { viewModelScope.launch { myUseCase().fold( onSuccess = { myData -> _uiState.emit( if (myData.isEmpty()) MyObjectState.Error else MyObjectState.Data(myData) ) }, onFailure = { _uiState.emit(MyObjectState.Error) } ) } } }
注:此方案仅保证同一ViewModel实例内API不重复调用,若Item销毁导致ViewModel被回收,重新进入时仍会发起请求(符合页面重新加载的预期)。
二、解决状态更新后LazyColumn不自动显示Item的问题
当uiState为Empty或Error时,你返回空布局{},LazyColumn会认为该Item高度为0,不会为其预留布局空间。当状态变为Data时,Composable尺寸变化,但LazyColumn不会自动重新计算布局,因此需要用户交互才会刷新。
解决方案:添加占位布局保持Item存在
在Empty/Error状态下返回高度为0的占位Box,让LazyColumn能跟踪Item的状态变化:
when (uiState) { is MyObjectState.Data -> { Surface(modifier = modifier) { // 数据展示UI } } is MyObjectState.Error, MyObjectState.Empty -> { // 占位布局,确保LazyColumn感知到Item的存在 Box(modifier = Modifier.height(0.dp)) } }
这样状态切换为Data时,LazyColumn会自动重新测量Item高度,无需用户交互即可显示内容。
关于改用Column的问题
如果改用Column,所有列表项会一次性全部初始化,不会出现Item销毁重建的情况,因此ViewModel只会创建一次,状态更新后也会立即刷新。但Column不适合长列表,会导致内存占用过高、渲染性能下降,因此不建议为解决此问题改用Column,优先使用上述LazyColumn的适配方案。
内容的提问来源于stack exchange,提问作者Skizo-ozᴉʞS ツ

