You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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的loadedImages StateFlow中收集当前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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 21:14:51