Composable初始组合时调用远程API并避免无限重组的最佳实践
问题分析
当前Composable函数Device中,直接在组件内部调用vm.getDevices()会引发无限重组:每次调用该方法会更新uiState,触发UI重组,重组后又会再次调用vm.getDevices(),形成循环。
原始代码
Composable组件代码
@Composable fun Device(contentPadding: PaddingValues, modifier: Modifier = Modifier) { val vm:DeviceList = viewModel() vm.getDevices() var devices = vm.uiState.collectAsState(); LazyColumn(contentPadding = contentPadding) { items(devices.value) { device -> DeviceItem(device) } } }
ViewModel代码
class DeviceList : ViewModel() { private var deviceListUIState: MutableStateFlow<List<Device>> = MutableStateFlow( listOf() ) val uiState get() = deviceListUIState.asStateFlow() fun getDevices() { viewModelScope.launch { try { val result: List<Device> = myApi.retrofitService.getDevices() deviceListUIState.value = result } catch (e: Exception) { Log.e(this.toString(), e.message ?: "") } } } }
最佳实践方案
以下是几种简洁优雅的解决方案,避免无限重组并符合Jetpack Compose的设计规范:
方案一:ViewModel初始化时自动加载数据
直接在ViewModel的初始化块中调用getDevices(),数据只会在ViewModel创建时加载一次,不会随UI重组重复执行。
修改后的ViewModel:
class DeviceList : ViewModel() { private var deviceListUIState: MutableStateFlow<List<Device>> = MutableStateFlow( listOf() ) val uiState get() = deviceListUIState.asStateFlow() // 初始化时自动触发数据加载 init { getDevices() } fun getDevices() { viewModelScope.launch { try { val result: List<Device> = myApi.retrofitService.getDevices() deviceListUIState.value = result } catch (e: Exception) { Log.e(this.toString(), e.message ?: "") } } } }
对应的Composable组件移除vm.getDevices()调用:
@Composable fun Device(contentPadding: PaddingValues, modifier: Modifier = Modifier) { val vm:DeviceList = viewModel() val devices = vm.uiState.collectAsState(); LazyColumn(contentPadding = contentPadding) { items(devices.value) { device -> DeviceItem(device) } } }
方案二:用LaunchedEffect触发单次加载
如果需要在Composable首次组合时才加载数据(而非ViewModel创建时),可以使用LaunchedEffect,传入固定不变的键(如Unit),确保代码仅执行一次。
修改后的Composable组件:
@Composable fun Device(contentPadding: PaddingValues, modifier: Modifier = Modifier) { val vm:DeviceList = viewModel() val devices = vm.uiState.collectAsState(); // 仅在组件首次组合时执行一次数据加载 LaunchedEffect(Unit) { vm.getDevices() } LazyColumn(contentPadding = contentPadding) { items(devices.value) { device -> DeviceItem(device) } } }
方案三:添加加载状态控制(支持手动刷新)
如果需要支持手动刷新,同时避免重复请求,可以在ViewModel中维护加载状态,确保同一时间只有一个请求在执行。
修改后的ViewModel:
class DeviceList : ViewModel() { private var deviceListUIState: MutableStateFlow<List<Device>> = MutableStateFlow( listOf() ) // 维护加载状态,防止重复请求 private val _isLoading = MutableStateFlow(false) val isLoading = _isLoading.asStateFlow() val uiState get() = deviceListUIState.asStateFlow() fun getDevices() { // 正在加载时直接返回,避免重复触发 if (_isLoading.value) return viewModelScope.launch { _isLoading.value = true try { val result: List<Device> = myApi.retrofitService.getDevices() deviceListUIState.value = result } catch (e: Exception) { Log.e(this.toString(), e.message ?: "") } finally { _isLoading.value = false } } } }
对应的Composable组件:
@Composable fun Device(contentPadding: PaddingValues, modifier: Modifier = Modifier) { val vm:DeviceList = viewModel() val devices = vm.uiState.collectAsState() val isLoading = vm.isLoading.collectAsState() // 首次组合加载数据 LaunchedEffect(Unit) { vm.getDevices() } Column(modifier = modifier) { // 显示加载状态 if (isLoading.value) { CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) } LazyColumn(contentPadding = contentPadding) { items(devices.value) { device -> DeviceItem(device) } } // 手动刷新按钮 Button( onClick = { vm.getDevices() }, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text("刷新设备列表") } } }
内容的提问来源于stack exchange,提问作者A Farmanbar
相关产品推荐
相关产品推荐

