如何在LazyColumn的Item中订阅已缓存大图的更新数据
LazyColumn大型图片缓存后UI更新问题解决方案
问题背景
需要在LazyColumn中展示从亚马逊服务器拉取的最大5MB的图片,因服务器生成唯一URL无法使用Coil,只能先缓存到设备再显示。最初在每个LazyColumn项的LaunchedEffect中触发下载,出现OOM错误:
java.lang.OutOfMemoryError: Failed to allocate a 16-byte allocation with 199,440 free bytes and 194KB until OOM, target footprint 201326592, growth limit 201326592; giving up on allocation because <1% of heap free after GC.
改用协程顺序下载后解决了OOM和卡顿,但ItemPhoto无法正确订阅更新,不知道对应ID的图片已完成缓存。
原始代码
@HiltViewModel class BigFileViewModel @Inject constructor( val serviceApi : ServiceApi ) : ViewModel(){ private val channel = Channel<DownloadImage>() var loadedImagesList = mutableStateOf<List<DownloadImage>?>(null) private set init{ CoroutineScope(Dispatchers.Default).launch { for (operation in channel) { CoroutineScope(Dispatchers.Default).async { serviceApi.downloadFileLink(operation.imageLink).collect{response-> if(response is NetworkResponse.Success){ updateItem(DownloadImage(operation.itemId, response.data)) } } } } } } private fun updateItem(downloadImage: DownloadImage) { loadedImagesList .value = loadedImagesList .value.orEmpty() + downloadImage } fun downloadPhoto(itemId : Int, url : String){ viewModelScope.launch { channel.send(DownloadImage(itemId, url)) } } } @Composable fun BigFileList(bigFileViewModel : BigFileViewModel = hiltViewModel()){ LazyColumn(content = { items(1){ FlowRowItem(bigFileViewModel) } }) } @OptIn(ExperimentalLayoutApi::class) @Composable fun FlowRowItem( bigFileViewModel : BigFileViewModel){ FlowRow( modifier = Modifier .padding(8.dp), horizontalArrangement = Arrangement.Start ) { for(item in 0..23){ ItemPhoto((0..10000).random(), imageList.random(), bigFileViewModel) } } } @Composable fun ItemPhoto(itemId : Int, setLink : String, bigFileViewModel : BigFileViewModel){ val link = remember { mutableStateOf<String?>( null ) } val updateList by bigFileViewModel.loadedImagesList LaunchedEffect(key1 = Unit, block = { bigFileViewModel.downloadPhoto(itemId, setLink) }) LaunchedEffect(key1 = updateList, block = { updateList?.forEach {item-> if(item.itemId == itemId){ link.value = item.imageLink } } }) link.value?.let {url-> Image( painter = rememberAsyncImagePainter( url, filterQuality = FilterQuality.None, ), contentDescription = null, modifier = itemModifier2, contentScale = ContentScale.Crop ) } ?: run { Box(contentAlignment = Alignment.Center) { Column( modifier = itemModifier2, verticalArrangement = Arrangement.spacedBy(5.dp, Alignment.Top), horizontalAlignment = Alignment.CenterHorizontally, ) { CircularProgressIndicator() Text("$itemId") } } } }
解决方案
1. 优化ViewModel状态管理
- 用
StateFlow<Map<Int, String>>替代mutableStateOf<List<DownloadImage>?>,通过itemId快速索引已下载的图片路径,避免遍历整个列表 - 修正协程处理逻辑,确保顺序下载且不创建多余协程
- 使用
viewModelScope而非自定义CoroutineScope,避免内存泄漏
修改后的ViewModel:
@HiltViewModel class BigFileViewModel @Inject constructor( val serviceApi: ServiceApi ) : ViewModel() { private val channel = Channel<DownloadRequest>() private val _loadedImages = MutableStateFlow<Map<Int, String>>(emptyMap()) val loadedImages: StateFlow<Map<Int, String>> = _loadedImages init { viewModelScope.launch(Dispatchers.IO) { for (request in channel) { // 顺序处理下载请求,避免并发OOM serviceApi.downloadFileLink(request.url).collect { response -> if (response is NetworkResponse.Success) { _loadedImages.update { current -> current + (request.itemId to response.data) } } } } } } fun downloadPhoto(itemId: Int, url: String) { viewModelScope.launch { // 避免重复发送相同itemId的下载请求 if (!_loadedImages.value.containsKey(itemId)) { channel.send(DownloadRequest(itemId, url)) } } } data class DownloadRequest(val itemId: Int, val url: String) } // 保留原DownloadImage类,或直接用DownloadRequest替代(根据实际需求) data class DownloadImage(val itemId: Int, val imageLink: String)
2. 简化ItemPhoto的订阅逻辑
- 直接从ViewModel的
loadedImagesStateFlow中收集当前itemId对应的路径,无需中间状态 - 使用
collectAsStateWithLifecycle自动处理生命周期,确保在组件不可见时停止订阅 - 去掉冗余的LaunchedEffect,简化UI逻辑
修改后的ItemPhoto:
@Composable fun ItemPhoto(itemId: Int, setLink: String, bigFileViewModel: BigFileViewModel) { // 收集当前itemId对应的已下载图片路径 val loadedImages by bigFileViewModel.loadedImages.collectAsStateWithLifecycle() val cachedUrl = loadedImages[itemId] // 仅在未缓存时触发下载 LaunchedEffect(itemId) { if (cachedUrl == null) { bigFileViewModel.downloadPhoto(itemId, setLink) } } cachedUrl?.let { url -> Image( painter = rememberAsyncImagePainter( url, filterQuality = FilterQuality.None, ), contentDescription = null, modifier = itemModifier2, contentScale = ContentScale.Crop ) } ?: run { Box(contentAlignment = Alignment.Center, modifier = itemModifier2) { Column( verticalArrangement = Arrangement.spacedBy(5.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { CircularProgressIndicator() Text("$itemId") } } } }
3. 其他优化点
FlowRowItem中生成的随机itemId可能重复,建议使用唯一标识避免重复下载- 增加下载失败的处理逻辑(比如重试),当前代码仅处理成功情况
为什么这样改?
- Map替代List:通过itemId直接查找缓存路径,时间复杂度从O(n)降到O(1),提升UI更新效率
- StateFlow:更适合跨组件的状态订阅,配合
collectAsStateWithLifecycle遵循Jetpack生命周期最佳实践,避免内存泄漏 - 避免重复请求:在发送下载请求前检查是否已缓存,减少不必要的网络请求
- 简化UI逻辑:去掉中间状态
link,让UI直接响应ViewModel的状态变化,减少出错概率
内容的提问来源于stack exchange,提问作者Segrei Ulanov
相关产品推荐
相关产品推荐

